@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,492 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2018 The boardgame.io Authors
|
|
3
|
+
*
|
|
4
|
+
* Use of this source code is governed by a MIT-style
|
|
5
|
+
* license that can be found in the LICENSE file or at
|
|
6
|
+
* https://opensource.org/licenses/MIT.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
CreateGameReducer,
|
|
11
|
+
TransientHandlingMiddleware,
|
|
12
|
+
} from '../core/reducer';
|
|
13
|
+
import { ProcessGameConfig, IsLongFormMove } from '../core/game';
|
|
14
|
+
import { UNDO, REDO, MAKE_MOVE } from '../core/action-types';
|
|
15
|
+
import { createStore, applyMiddleware } from 'redux';
|
|
16
|
+
import * as logging from '../core/logger';
|
|
17
|
+
import type {
|
|
18
|
+
SyncInfo,
|
|
19
|
+
FilteredMetadata,
|
|
20
|
+
Game,
|
|
21
|
+
Server,
|
|
22
|
+
State,
|
|
23
|
+
ActionShape,
|
|
24
|
+
CredentialedActionShape,
|
|
25
|
+
LogEntry,
|
|
26
|
+
PlayerID,
|
|
27
|
+
ChatMessage,
|
|
28
|
+
} from '../types';
|
|
29
|
+
import { createMatch } from '../server/util';
|
|
30
|
+
import type { Auth } from '../server/auth';
|
|
31
|
+
import * as StorageAPI from '../server/db/base';
|
|
32
|
+
import type { Operation } from 'rfc6902';
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Filter match data to get a player metadata object with credentials stripped.
|
|
36
|
+
*/
|
|
37
|
+
const filterMatchData = (matchData: Server.MatchData): FilteredMetadata =>
|
|
38
|
+
Object.values(matchData.players).map((player) => {
|
|
39
|
+
const { credentials, ...filteredData } = player;
|
|
40
|
+
return filteredData;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Remove player credentials from action payload
|
|
45
|
+
*/
|
|
46
|
+
const stripCredentialsFromAction = (action: CredentialedActionShape.Any) => {
|
|
47
|
+
const { credentials, ...payload } = action.payload;
|
|
48
|
+
return { ...action, payload };
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
type CallbackFn = (arg: {
|
|
52
|
+
state: State;
|
|
53
|
+
matchID: string;
|
|
54
|
+
action?: ActionShape.Any | CredentialedActionShape.Any;
|
|
55
|
+
}) => void;
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Data types that are shared across `TransportData` and `IntermediateTransportData`.
|
|
59
|
+
*/
|
|
60
|
+
type CommonTransportData =
|
|
61
|
+
| {
|
|
62
|
+
type: 'sync';
|
|
63
|
+
args: [string, SyncInfo];
|
|
64
|
+
}
|
|
65
|
+
| {
|
|
66
|
+
type: 'matchData';
|
|
67
|
+
args: [string, FilteredMetadata];
|
|
68
|
+
}
|
|
69
|
+
| {
|
|
70
|
+
type: 'chat';
|
|
71
|
+
args: [string, ChatMessage];
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Final shape of data sent by the transport API
|
|
76
|
+
* to be received by clients/client transports.
|
|
77
|
+
*/
|
|
78
|
+
export type TransportData =
|
|
79
|
+
| {
|
|
80
|
+
type: 'update';
|
|
81
|
+
args: [string, State, LogEntry[]];
|
|
82
|
+
}
|
|
83
|
+
| {
|
|
84
|
+
type: 'patch';
|
|
85
|
+
args: [string, number, number, Operation[], LogEntry[]];
|
|
86
|
+
}
|
|
87
|
+
| CommonTransportData;
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Data type sent by a master to its transport API. The transport then transforms
|
|
91
|
+
* this into `TransportData` for each individual player it forwards it to.
|
|
92
|
+
*/
|
|
93
|
+
export type IntermediateTransportData =
|
|
94
|
+
| {
|
|
95
|
+
type: 'update';
|
|
96
|
+
args: [string, State];
|
|
97
|
+
}
|
|
98
|
+
| {
|
|
99
|
+
type: 'patch';
|
|
100
|
+
args: [string, number, State, State];
|
|
101
|
+
}
|
|
102
|
+
| CommonTransportData;
|
|
103
|
+
|
|
104
|
+
/** API used by a master to emit data to any connected clients/client transports. */
|
|
105
|
+
export interface TransportAPI {
|
|
106
|
+
send: (
|
|
107
|
+
playerData: { playerID: PlayerID } & IntermediateTransportData
|
|
108
|
+
) => void;
|
|
109
|
+
sendAll: (payload: IntermediateTransportData) => void;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Master
|
|
114
|
+
*
|
|
115
|
+
* Class that runs the game and maintains the authoritative state.
|
|
116
|
+
* It uses the transportAPI to communicate with clients and the
|
|
117
|
+
* storageAPI to communicate with the database.
|
|
118
|
+
*/
|
|
119
|
+
export class Master {
|
|
120
|
+
game: ReturnType<typeof ProcessGameConfig>;
|
|
121
|
+
storageAPI: StorageAPI.Sync | StorageAPI.Async;
|
|
122
|
+
transportAPI: TransportAPI;
|
|
123
|
+
subscribeCallback: CallbackFn;
|
|
124
|
+
auth?: Auth;
|
|
125
|
+
|
|
126
|
+
constructor(
|
|
127
|
+
game: Game,
|
|
128
|
+
storageAPI: StorageAPI.Sync | StorageAPI.Async,
|
|
129
|
+
transportAPI: TransportAPI,
|
|
130
|
+
auth?: Auth
|
|
131
|
+
) {
|
|
132
|
+
this.game = ProcessGameConfig(game);
|
|
133
|
+
this.storageAPI = storageAPI;
|
|
134
|
+
this.transportAPI = transportAPI;
|
|
135
|
+
this.subscribeCallback = () => {};
|
|
136
|
+
this.auth = auth;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
subscribe(fn: CallbackFn) {
|
|
140
|
+
this.subscribeCallback = fn;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Called on each move / event made by the client.
|
|
145
|
+
* Computes the new value of the game state and returns it
|
|
146
|
+
* along with a deltalog.
|
|
147
|
+
*/
|
|
148
|
+
async onUpdate(
|
|
149
|
+
credAction: CredentialedActionShape.Any,
|
|
150
|
+
stateID: number,
|
|
151
|
+
matchID: string,
|
|
152
|
+
playerID: string
|
|
153
|
+
): Promise<void | { error: string }> {
|
|
154
|
+
if (!credAction || !credAction.payload) {
|
|
155
|
+
return { error: 'missing action or action payload' };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
let metadata: Server.MatchData | undefined;
|
|
159
|
+
if (StorageAPI.isSynchronous(this.storageAPI)) {
|
|
160
|
+
({ metadata } = this.storageAPI.fetch(matchID, { metadata: true }));
|
|
161
|
+
} else {
|
|
162
|
+
({ metadata } = await this.storageAPI.fetch(matchID, { metadata: true }));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (this.auth) {
|
|
166
|
+
const isAuthentic = await this.auth.authenticateCredentials({
|
|
167
|
+
playerID,
|
|
168
|
+
credentials: credAction.payload.credentials,
|
|
169
|
+
metadata,
|
|
170
|
+
});
|
|
171
|
+
if (!isAuthentic) {
|
|
172
|
+
return { error: 'unauthorized action' };
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const action = stripCredentialsFromAction(credAction);
|
|
177
|
+
const key = matchID;
|
|
178
|
+
|
|
179
|
+
let state: State;
|
|
180
|
+
if (StorageAPI.isSynchronous(this.storageAPI)) {
|
|
181
|
+
({ state } = this.storageAPI.fetch(key, { state: true }));
|
|
182
|
+
} else {
|
|
183
|
+
({ state } = await this.storageAPI.fetch(key, { state: true }));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (state === undefined) {
|
|
187
|
+
logging.error(`game not found, matchID=[${key}]`);
|
|
188
|
+
return { error: 'game not found' };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (state.ctx.gameover !== undefined) {
|
|
192
|
+
logging.error(
|
|
193
|
+
`game over - matchID=[${key}] - playerID=[${playerID}]` +
|
|
194
|
+
` - action[${action.payload.type}]`
|
|
195
|
+
);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const reducer = CreateGameReducer({
|
|
200
|
+
game: this.game,
|
|
201
|
+
});
|
|
202
|
+
const middleware = applyMiddleware(TransientHandlingMiddleware);
|
|
203
|
+
const store = createStore(reducer, state, middleware);
|
|
204
|
+
|
|
205
|
+
// Only allow UNDO / REDO if there is exactly one player
|
|
206
|
+
// that can make moves right now and the person doing the
|
|
207
|
+
// action is that player.
|
|
208
|
+
if (action.type == UNDO || action.type == REDO) {
|
|
209
|
+
const hasActivePlayers = state.ctx.activePlayers !== null;
|
|
210
|
+
const isCurrentPlayer = state.ctx.currentPlayer === playerID;
|
|
211
|
+
|
|
212
|
+
if (
|
|
213
|
+
// If activePlayers is empty, non-current players can’t undo.
|
|
214
|
+
(!hasActivePlayers && !isCurrentPlayer) ||
|
|
215
|
+
// If player is not active or multiple players are active, can’t undo.
|
|
216
|
+
(hasActivePlayers &&
|
|
217
|
+
(state.ctx.activePlayers[playerID] === undefined ||
|
|
218
|
+
Object.keys(state.ctx.activePlayers).length > 1))
|
|
219
|
+
) {
|
|
220
|
+
logging.error(`playerID=[${playerID}] cannot undo / redo right now`);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Check whether the player is active.
|
|
226
|
+
if (!this.game.flow.isPlayerActive(state.G, state.ctx, playerID)) {
|
|
227
|
+
logging.error(
|
|
228
|
+
`player not active - playerID=[${playerID}]` +
|
|
229
|
+
` - action[${action.payload.type}]`
|
|
230
|
+
);
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Get move for further checks
|
|
235
|
+
const move =
|
|
236
|
+
action.type == MAKE_MOVE
|
|
237
|
+
? this.game.flow.getMove(state.ctx, action.payload.type, playerID)
|
|
238
|
+
: null;
|
|
239
|
+
|
|
240
|
+
// Check whether the player is allowed to make the move.
|
|
241
|
+
if (action.type == MAKE_MOVE && !move) {
|
|
242
|
+
logging.error(
|
|
243
|
+
`move not processed - canPlayerMakeMove=false - playerID=[${playerID}]` +
|
|
244
|
+
` - action[${action.payload.type}]`
|
|
245
|
+
);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Check if action's stateID is different than store's stateID
|
|
250
|
+
// and if move does not have ignoreStaleStateID truthy.
|
|
251
|
+
if (
|
|
252
|
+
state._stateID !== stateID &&
|
|
253
|
+
!(move && IsLongFormMove(move) && move.ignoreStaleStateID)
|
|
254
|
+
) {
|
|
255
|
+
logging.error(
|
|
256
|
+
`invalid stateID, was=[${stateID}], expected=[${state._stateID}]` +
|
|
257
|
+
` - playerID=[${playerID}] - action[${action.payload.type}]`
|
|
258
|
+
);
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const prevState = store.getState();
|
|
263
|
+
|
|
264
|
+
// Update server's version of the store.
|
|
265
|
+
store.dispatch(action);
|
|
266
|
+
state = store.getState();
|
|
267
|
+
|
|
268
|
+
this.subscribeCallback({
|
|
269
|
+
state,
|
|
270
|
+
action,
|
|
271
|
+
matchID,
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
if (this.game.deltaState) {
|
|
275
|
+
this.transportAPI.sendAll({
|
|
276
|
+
type: 'patch',
|
|
277
|
+
args: [matchID, stateID, prevState, state],
|
|
278
|
+
});
|
|
279
|
+
} else {
|
|
280
|
+
this.transportAPI.sendAll({
|
|
281
|
+
type: 'update',
|
|
282
|
+
args: [matchID, state],
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const { deltalog, ...stateWithoutDeltalog } = state;
|
|
287
|
+
|
|
288
|
+
let newMetadata: Server.MatchData | undefined;
|
|
289
|
+
if (
|
|
290
|
+
metadata &&
|
|
291
|
+
(metadata.gameover === undefined || metadata.gameover === null)
|
|
292
|
+
) {
|
|
293
|
+
newMetadata = {
|
|
294
|
+
...metadata,
|
|
295
|
+
updatedAt: Date.now(),
|
|
296
|
+
};
|
|
297
|
+
if (state.ctx.gameover !== undefined) {
|
|
298
|
+
newMetadata.gameover = state.ctx.gameover;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (StorageAPI.isSynchronous(this.storageAPI)) {
|
|
303
|
+
this.storageAPI.setState(key, stateWithoutDeltalog, deltalog);
|
|
304
|
+
if (newMetadata) this.storageAPI.setMetadata(key, newMetadata);
|
|
305
|
+
} else {
|
|
306
|
+
const writes = [
|
|
307
|
+
this.storageAPI.setState(key, stateWithoutDeltalog, deltalog),
|
|
308
|
+
];
|
|
309
|
+
if (newMetadata) {
|
|
310
|
+
writes.push(this.storageAPI.setMetadata(key, newMetadata));
|
|
311
|
+
}
|
|
312
|
+
await Promise.all(writes);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Called when the client connects / reconnects.
|
|
318
|
+
* Returns the latest game state and the entire log.
|
|
319
|
+
*/
|
|
320
|
+
async onSync(
|
|
321
|
+
matchID: string,
|
|
322
|
+
playerID: string | null | undefined,
|
|
323
|
+
credentials?: string,
|
|
324
|
+
numPlayers = 2
|
|
325
|
+
): Promise<void | { error: string }> {
|
|
326
|
+
const key = matchID;
|
|
327
|
+
|
|
328
|
+
const fetchOpts = {
|
|
329
|
+
state: true,
|
|
330
|
+
metadata: true,
|
|
331
|
+
log: true,
|
|
332
|
+
initialState: true,
|
|
333
|
+
} as const;
|
|
334
|
+
|
|
335
|
+
const fetchResult = StorageAPI.isSynchronous(this.storageAPI)
|
|
336
|
+
? this.storageAPI.fetch(key, fetchOpts)
|
|
337
|
+
: await this.storageAPI.fetch(key, fetchOpts);
|
|
338
|
+
|
|
339
|
+
let { state, initialState, log, metadata } = fetchResult;
|
|
340
|
+
|
|
341
|
+
if (this.auth && playerID !== undefined && playerID !== null) {
|
|
342
|
+
const isAuthentic = await this.auth.authenticateCredentials({
|
|
343
|
+
playerID,
|
|
344
|
+
credentials,
|
|
345
|
+
metadata,
|
|
346
|
+
});
|
|
347
|
+
if (!isAuthentic) {
|
|
348
|
+
return { error: 'unauthorized' };
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// If the game doesn't exist, then create one on demand.
|
|
353
|
+
// TODO: Move this out of the sync call.
|
|
354
|
+
if (state === undefined) {
|
|
355
|
+
const match = createMatch({
|
|
356
|
+
game: this.game,
|
|
357
|
+
unlisted: true,
|
|
358
|
+
numPlayers,
|
|
359
|
+
setupData: undefined,
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
if ('setupDataError' in match) {
|
|
363
|
+
return { error: 'game requires setupData' };
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
initialState = state = match.initialState;
|
|
367
|
+
metadata = match.metadata;
|
|
368
|
+
|
|
369
|
+
this.subscribeCallback({ state, matchID });
|
|
370
|
+
|
|
371
|
+
if (StorageAPI.isSynchronous(this.storageAPI)) {
|
|
372
|
+
this.storageAPI.createMatch(key, { initialState, metadata });
|
|
373
|
+
} else {
|
|
374
|
+
await this.storageAPI.createMatch(key, { initialState, metadata });
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const filteredMetadata = metadata ? filterMatchData(metadata) : undefined;
|
|
379
|
+
|
|
380
|
+
const syncInfo: SyncInfo = {
|
|
381
|
+
state,
|
|
382
|
+
log,
|
|
383
|
+
filteredMetadata,
|
|
384
|
+
initialState,
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
this.transportAPI.send({
|
|
388
|
+
playerID,
|
|
389
|
+
type: 'sync',
|
|
390
|
+
args: [matchID, syncInfo],
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Called when a client connects or disconnects.
|
|
398
|
+
* Updates and sends out metadata to reflect the player’s connection status.
|
|
399
|
+
*/
|
|
400
|
+
async onConnectionChange(
|
|
401
|
+
matchID: string,
|
|
402
|
+
playerID: string | null | undefined,
|
|
403
|
+
credentials: string | undefined,
|
|
404
|
+
connected: boolean
|
|
405
|
+
): Promise<void | { error: string }> {
|
|
406
|
+
const key = matchID;
|
|
407
|
+
|
|
408
|
+
// Ignore changes for clients without a playerID, e.g. spectators.
|
|
409
|
+
if (playerID === undefined || playerID === null) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
let metadata: Server.MatchData | undefined;
|
|
414
|
+
|
|
415
|
+
if (StorageAPI.isSynchronous(this.storageAPI)) {
|
|
416
|
+
({ metadata } = this.storageAPI.fetch(key, { metadata: true }));
|
|
417
|
+
} else {
|
|
418
|
+
({ metadata } = await this.storageAPI.fetch(key, { metadata: true }));
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
if (metadata === undefined) {
|
|
422
|
+
logging.error(`metadata not found for matchID=[${key}]`);
|
|
423
|
+
return { error: 'metadata not found' };
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (metadata.players[playerID] === undefined) {
|
|
427
|
+
logging.error(
|
|
428
|
+
`Player not in the match, matchID=[${key}] playerID=[${playerID}]`
|
|
429
|
+
);
|
|
430
|
+
return { error: 'player not in the match' };
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (this.auth) {
|
|
434
|
+
const isAuthentic = await this.auth.authenticateCredentials({
|
|
435
|
+
playerID,
|
|
436
|
+
credentials,
|
|
437
|
+
metadata,
|
|
438
|
+
});
|
|
439
|
+
if (!isAuthentic) {
|
|
440
|
+
return { error: 'unauthorized' };
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
metadata.players[playerID].isConnected = connected;
|
|
445
|
+
|
|
446
|
+
const filteredMetadata = filterMatchData(metadata);
|
|
447
|
+
|
|
448
|
+
this.transportAPI.sendAll({
|
|
449
|
+
type: 'matchData',
|
|
450
|
+
args: [matchID, filteredMetadata],
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
if (StorageAPI.isSynchronous(this.storageAPI)) {
|
|
454
|
+
this.storageAPI.setMetadata(key, metadata);
|
|
455
|
+
} else {
|
|
456
|
+
await this.storageAPI.setMetadata(key, metadata);
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
async onChatMessage(
|
|
461
|
+
matchID: string,
|
|
462
|
+
chatMessage: ChatMessage,
|
|
463
|
+
credentials: string | undefined
|
|
464
|
+
): Promise<void | { error: string }> {
|
|
465
|
+
const key = matchID;
|
|
466
|
+
|
|
467
|
+
if (this.auth) {
|
|
468
|
+
const { metadata } = await (this.storageAPI as StorageAPI.Async).fetch(
|
|
469
|
+
key,
|
|
470
|
+
{
|
|
471
|
+
metadata: true,
|
|
472
|
+
}
|
|
473
|
+
);
|
|
474
|
+
if (!(chatMessage && typeof chatMessage.sender === 'string')) {
|
|
475
|
+
return { error: 'unauthorized' };
|
|
476
|
+
}
|
|
477
|
+
const isAuthentic = await this.auth.authenticateCredentials({
|
|
478
|
+
playerID: chatMessage.sender,
|
|
479
|
+
credentials,
|
|
480
|
+
metadata,
|
|
481
|
+
});
|
|
482
|
+
if (!isAuthentic) {
|
|
483
|
+
return { error: 'unauthorized' };
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
this.transportAPI.sendAll({
|
|
488
|
+
type: 'chat',
|
|
489
|
+
args: [matchID, chatMessage],
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2018 The boardgame.io Authors
|
|
3
|
+
*
|
|
4
|
+
* Use of this source code is governed by a MIT-style
|
|
5
|
+
* license that can be found in the LICENSE file or at
|
|
6
|
+
* https://opensource.org/licenses/MIT.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { Events } from './events';
|
|
10
|
+
import { Client } from '../../client/client';
|
|
11
|
+
import { error } from '../../core/logger';
|
|
12
|
+
import type { Game, Ctx } from '../../types';
|
|
13
|
+
|
|
14
|
+
jest.mock('../../core/logger', () => ({
|
|
15
|
+
info: jest.fn(),
|
|
16
|
+
error: jest.fn(),
|
|
17
|
+
}));
|
|
18
|
+
afterEach(jest.clearAllMocks);
|
|
19
|
+
|
|
20
|
+
test('constructor', () => {
|
|
21
|
+
const flow = {} as Game['flow'];
|
|
22
|
+
const playerID = '0';
|
|
23
|
+
const e = new Events(flow, { phase: '', turn: 0 } as Ctx, playerID);
|
|
24
|
+
expect(e.flow).toBe(flow);
|
|
25
|
+
expect(e.playerID).toBe(playerID);
|
|
26
|
+
expect(e.dispatch).toEqual([]);
|
|
27
|
+
expect(e.initialTurn).toEqual(0);
|
|
28
|
+
expect(e.currentPhase).toEqual('');
|
|
29
|
+
expect(e.currentTurn).toEqual(0);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('dispatch', () => {
|
|
33
|
+
const flow = { eventNames: ['A', 'B'] } as Game['flow'];
|
|
34
|
+
const e = new Events(flow, { phase: '', turn: 0 } as Ctx);
|
|
35
|
+
const events = e.api();
|
|
36
|
+
|
|
37
|
+
expect(e.dispatch).toEqual([]);
|
|
38
|
+
(events as unknown as { A(): void }).A();
|
|
39
|
+
(events as unknown as { B(): void }).B();
|
|
40
|
+
expect(e.dispatch).toEqual([
|
|
41
|
+
{
|
|
42
|
+
type: 'A',
|
|
43
|
+
args: [],
|
|
44
|
+
phase: '',
|
|
45
|
+
turn: 0,
|
|
46
|
+
calledFrom: undefined,
|
|
47
|
+
error: expect.any(Error),
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
type: 'B',
|
|
51
|
+
args: [],
|
|
52
|
+
phase: '',
|
|
53
|
+
turn: 0,
|
|
54
|
+
calledFrom: undefined,
|
|
55
|
+
error: expect.any(Error),
|
|
56
|
+
},
|
|
57
|
+
]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test('update ctx', () => {
|
|
61
|
+
const game: Game = {
|
|
62
|
+
moves: {
|
|
63
|
+
A: ({ G, events }) => {
|
|
64
|
+
events.endTurn();
|
|
65
|
+
return G;
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
const client = Client({ game });
|
|
70
|
+
expect(client.getState().ctx.turn).toBe(1);
|
|
71
|
+
client.moves.A();
|
|
72
|
+
expect(client.getState().ctx.turn).toBe(2);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('no duplicate endTurn', () => {
|
|
76
|
+
const game: Game = {
|
|
77
|
+
turn: {
|
|
78
|
+
onEnd: ({ events }) => {
|
|
79
|
+
events.endTurn();
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
const client = Client({ game });
|
|
84
|
+
expect(client.getState().ctx.turn).toBe(1);
|
|
85
|
+
client.events.endTurn();
|
|
86
|
+
expect(client.getState().ctx.turn).toBe(1);
|
|
87
|
+
expect(error).toHaveBeenCalled();
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('no duplicate endPhase', () => {
|
|
91
|
+
const game: Game = {
|
|
92
|
+
phases: {
|
|
93
|
+
A: {
|
|
94
|
+
start: true,
|
|
95
|
+
onEnd: ({ events }) => {
|
|
96
|
+
events.setPhase('C');
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
B: {},
|
|
100
|
+
C: {},
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
const client = Client({ game });
|
|
104
|
+
expect(client.getState().ctx.phase).toBe('A');
|
|
105
|
+
client.events.setPhase('B');
|
|
106
|
+
expect(client.getState().ctx.phase).toBe('A');
|
|
107
|
+
expect(error).toHaveBeenCalled();
|
|
108
|
+
});
|