Skip to content

Commit 1688639

Browse files
committed
decouple transport layer from server logic
- create a GameMaster class that handles server side logic - create a transport layer API that can be overriden
1 parent 66aebf0 commit 1688639

6 files changed

Lines changed: 792 additions & 521 deletions

File tree

src/master/master.js

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/*
2+
* Copyright 2018 The boardgame.io Authors
3+
*
4+
* Use of this source code is governed by a MIT-style
5+
* license that can be found in the LICENSE file or at
6+
* https://opensource.org/licenses/MIT.
7+
*/
8+
9+
import { CreateGameReducer } from '../core/reducer';
10+
import { MAKE_MOVE, GAME_EVENT } from '../core/action-types';
11+
import { createStore } from 'redux';
12+
13+
export class GameMaster {
14+
constructor(game, storageAPI, transportAPI, isActionFromAuthenticPlayer) {
15+
this.game = game;
16+
this.storageAPI = storageAPI;
17+
this.transportAPI = transportAPI;
18+
this.isActionFromAuthenticPlayer = () => true;
19+
20+
if (isActionFromAuthenticPlayer !== undefined) {
21+
this.isActionFromAuthenticPlayer = isActionFromAuthenticPlayer;
22+
}
23+
}
24+
25+
async onUpdate(action, stateID, gameID, playerID) {
26+
let state = await this.storageAPI.get(gameID);
27+
28+
if (state === undefined) {
29+
return { error: 'game not found' };
30+
}
31+
32+
const reducer = CreateGameReducer({
33+
game: this.game,
34+
numPlayers: state.ctx.numPlayers,
35+
});
36+
const store = createStore(reducer, state);
37+
38+
const isActionAuthentic = await this.isActionFromAuthenticPlayer({
39+
action,
40+
storageAPI: this.storageAPI,
41+
gameID,
42+
playerID,
43+
});
44+
if (!isActionAuthentic) {
45+
return { error: 'unauthorized action' };
46+
}
47+
48+
// Check whether the player is allowed to make the move.
49+
if (
50+
action.type == MAKE_MOVE &&
51+
!this.game.flow.canPlayerMakeMove(state.G, state.ctx, playerID)
52+
) {
53+
return;
54+
}
55+
56+
// Check whether the player is allowed to call the event.
57+
if (
58+
action.type == GAME_EVENT &&
59+
!this.game.flow.canPlayerCallEvent(state.G, state.ctx, playerID)
60+
) {
61+
return;
62+
}
63+
64+
if (state._stateID == stateID) {
65+
let log = store.getState().log || [];
66+
67+
// Update server's version of the store.
68+
store.dispatch(action);
69+
state = store.getState();
70+
71+
this.transportAPI.sendAll(playerID => {
72+
const filteredState = {
73+
...state,
74+
G: this.game.playerView(state.G, state.ctx, playerID),
75+
ctx: { ...state.ctx, _random: undefined },
76+
log: undefined,
77+
deltalog: undefined,
78+
};
79+
80+
return {
81+
type: 'update',
82+
args: [gameID, filteredState, state.deltalog],
83+
};
84+
});
85+
86+
// TODO: We currently attach the log back into the state
87+
// object before storing it, but this should probably
88+
// sit in a different part of the database eventually.
89+
log = [...log, ...state.deltalog];
90+
const stateWithLog = { ...state, log };
91+
92+
await this.storageAPI.set(gameID, stateWithLog);
93+
}
94+
95+
return;
96+
}
97+
98+
async onSync(gameID, playerID, numPlayers) {
99+
const reducer = CreateGameReducer({ game: this.game, numPlayers });
100+
let state = await this.storageAPI.get(gameID);
101+
102+
if (state === undefined) {
103+
const store = createStore(reducer);
104+
state = store.getState();
105+
await this.storageAPI.set(gameID, state);
106+
}
107+
108+
const filteredState = {
109+
...state,
110+
G: this.game.playerView(state.G, state.ctx, playerID),
111+
ctx: { ...state.ctx, _random: undefined },
112+
log: undefined,
113+
deltalog: undefined,
114+
};
115+
116+
this.transportAPI.send({
117+
playerID,
118+
type: 'sync',
119+
args: [gameID, filteredState, state.log],
120+
});
121+
122+
return;
123+
}
124+
}

0 commit comments

Comments
 (0)