Skip to content

Commit c4a11a7

Browse files
committed
ignore events from all but currentPlayer
moves may still be made by other players in the actionPlayers set
1 parent 342977b commit c4a11a7

6 files changed

Lines changed: 82 additions & 37 deletions

File tree

docs/turn-order.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ ctx: {
1717
}
1818
```
1919

20-
`currentPlayer` is basically the owner of the current turn.
20+
`currentPlayer` is basically the owner of the current turn,
21+
and the only player that can call events like `endTurn` and
22+
`endPhase`.
2123

2224
`actionPlayers` are the set of players that can currently
2325
make a move. It defaults to a list containing just the

src/core/flow.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,10 @@ export function Flow({
9595

9696
optimisticUpdate,
9797

98+
canPlayerCallEvent: (G, ctx, playerID) => {
99+
return ctx.currentPlayer == playerID;
100+
},
101+
98102
canPlayerMakeMove: (G, ctx, playerID) => {
99103
const actionPlayers = ctx.actionPlayers || [];
100104
return actionPlayers.includes(playerID) || actionPlayers.includes('any');

src/core/reducer.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,19 @@ export function CreateGameReducer({ game, numPlayers, multiplayer }) {
9494
return state;
9595
}
9696

97+
// Ignore the event if the player isn't allowed to make it.
98+
if (
99+
action.payload.playerID !== null &&
100+
action.payload.playerID !== undefined &&
101+
!game.flow.canPlayerCallEvent(
102+
state.G,
103+
state.ctx,
104+
action.payload.playerID
105+
)
106+
) {
107+
return state;
108+
}
109+
97110
// Initialize PRNG from ctx.
98111
const random = new Random(state.ctx);
99112
// Initialize Events API.
@@ -128,7 +141,7 @@ export function CreateGameReducer({ game, numPlayers, multiplayer }) {
128141
return state;
129142
}
130143

131-
// Ignore the move if the player cannot make it at this point.
144+
// Ignore the move if the player isn't allowed to make it.
132145
if (
133146
action.payload.playerID !== null &&
134147
action.payload.playerID !== undefined &&

src/core/reducer.test.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,10 @@ test('disable move by invalid playerIDs', () => {
8383
state = reducer(state, makeMove('A', null, '1'));
8484
expect(state._stateID).toBe(0);
8585

86+
// playerID="1" cannot call events right now.
87+
state = reducer(state, gameEvent('endTurn', null, '1'));
88+
expect(state._stateID).toBe(0);
89+
8690
// playerID="0" can move.
8791
state = reducer(state, makeMove('A', null, '0'));
8892
expect(state._stateID).toBe(1);

src/server/index.js

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const Redux = require('redux');
1212

1313
import { DBFromEnv } from './db';
1414
import { CreateGameReducer } from '../core/reducer';
15+
import { MAKE_MOVE, GAME_EVENT } from '../core/action-types';
1516
import { createApiServer, isActionFromAuthenticPlayer } from './api';
1617

1718
const PING_TIMEOUT = 20 * 1e3;
@@ -64,8 +65,19 @@ export function Server({ games, db, _clientInfo, _roomInfo }) {
6465
return { error: 'unauthorized action' };
6566
}
6667

67-
// Check whether the player is allowed to make the move
68-
if (!game.flow.canPlayerMakeMove(state.G, state.ctx, playerID)) {
68+
// Check whether the player is allowed to make the move.
69+
if (
70+
action.type == MAKE_MOVE &&
71+
!game.flow.canPlayerMakeMove(state.G, state.ctx, playerID)
72+
) {
73+
return;
74+
}
75+
76+
// Check whether the player is allowed to call the event.
77+
if (
78+
action.type == GAME_EVENT &&
79+
!game.flow.canPlayerCallEvent(state.G, state.ctx, playerID)
80+
) {
6981
return;
7082
}
7183

src/server/index.test.js

Lines changed: 43 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -255,52 +255,62 @@ test('action', async () => {
255255
// ... and not if player != currentPlayer
256256
await io.socket.receive('action', action, 1, 'gameID', '100');
257257
expect(io.socket.emit).toHaveBeenCalledTimes(0);
258+
await io.socket.receive(
259+
'action',
260+
ActionCreators.makeMove(),
261+
1,
262+
'gameID',
263+
'100'
264+
);
265+
expect(io.socket.emit).toHaveBeenCalledTimes(0);
258266

259267
// Another broadcasted action.
260268
await io.socket.receive('action', action, 1, 'gameID', '1');
261269
expect(io.socket.emit).toHaveBeenCalledTimes(2);
262270
});
263271

264-
test('playerView (sync)', async () => {
265-
// Write the player into G.
266-
const game = Game({
267-
playerView: (G, ctx, player) => {
268-
return Object.assign({}, G, { player });
269-
},
270-
});
271-
272-
const server = Server({ games: [game] });
273-
const io = server.app.context.io;
272+
describe('playerView', () => {
273+
test('sync', async () => {
274+
// Write the player into G.
275+
const game = Game({
276+
playerView: (G, ctx, player) => {
277+
return Object.assign({}, G, { player });
278+
},
279+
});
274280

275-
await io.socket.receive('sync', 'gameID', 0);
276-
expect(io.socket.emit).toHaveBeenCalledTimes(1);
277-
expect(io.socket.emit.mock.calls[0][2].G).toEqual({ player: 0 });
278-
});
281+
const server = Server({ games: [game] });
282+
const io = server.app.context.io;
279283

280-
test('playerView (action)', async () => {
281-
const game = Game({
282-
playerView: (G, ctx, player) => {
283-
return Object.assign({}, G, { player });
284-
},
284+
await io.socket.receive('sync', 'gameID', 0);
285+
expect(io.socket.emit).toHaveBeenCalledTimes(1);
286+
expect(io.socket.emit.mock.calls[0][2].G).toEqual({ player: 0 });
285287
});
286-
const server = Server({ games: [game] });
287-
const io = server.app.context.io;
288-
const action = ActionCreators.gameEvent('endTurn');
289288

290-
io.socket.id = 'first';
291-
await io.socket.receive('sync', 'gameID', '0', 2);
292-
io.socket.id = 'second';
293-
await io.socket.receive('sync', 'gameID', '1', 2);
294-
io.socket.emit.mockReset();
289+
test('action', async () => {
290+
const game = Game({
291+
playerView: (G, ctx, player) => {
292+
return Object.assign({}, G, { player });
293+
},
294+
});
295+
const server = Server({ games: [game] });
296+
const io = server.app.context.io;
297+
const action = ActionCreators.gameEvent('endTurn');
295298

296-
await io.socket.receive('action', action, 0, 'gameID', '0');
297-
expect(io.socket.emit).toHaveBeenCalledTimes(2);
299+
io.socket.id = 'first';
300+
await io.socket.receive('sync', 'gameID', '0', 2);
301+
io.socket.id = 'second';
302+
await io.socket.receive('sync', 'gameID', '1', 2);
303+
io.socket.emit.mockReset();
304+
305+
await io.socket.receive('action', action, 0, 'gameID', '0');
306+
expect(io.socket.emit).toHaveBeenCalledTimes(2);
298307

299-
const G_player0 = io.socket.emit.mock.calls[0][2].G;
300-
const G_player1 = io.socket.emit.mock.calls[1][2].G;
308+
const G_player0 = io.socket.emit.mock.calls[0][2].G;
309+
const G_player1 = io.socket.emit.mock.calls[1][2].G;
301310

302-
expect(G_player0.player).toBe('0');
303-
expect(G_player1.player).toBe('1');
311+
expect(G_player0.player).toBe('0');
312+
expect(G_player1.player).toBe('1');
313+
});
304314
});
305315

306316
test('custom db implementation', async () => {

0 commit comments

Comments
 (0)