Skip to content

Commit

Permalink
ignore events from all but currentPlayer
Browse files Browse the repository at this point in the history
moves may still be made by other players in the actionPlayers set
  • Loading branch information
nicolodavis committed Jul 28, 2018
1 parent 342977b commit c4a11a7
Show file tree
Hide file tree
Showing 6 changed files with 82 additions and 37 deletions.
4 changes: 3 additions & 1 deletion docs/turn-order.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ ctx: {
}
```

`currentPlayer` is basically the owner of the current turn.
`currentPlayer` is basically the owner of the current turn,
and the only player that can call events like `endTurn` and
`endPhase`.

`actionPlayers` are the set of players that can currently
make a move. It defaults to a list containing just the
Expand Down
4 changes: 4 additions & 0 deletions src/core/flow.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ export function Flow({

optimisticUpdate,

canPlayerCallEvent: (G, ctx, playerID) => {
return ctx.currentPlayer == playerID;
},

canPlayerMakeMove: (G, ctx, playerID) => {
const actionPlayers = ctx.actionPlayers || [];
return actionPlayers.includes(playerID) || actionPlayers.includes('any');
Expand Down
15 changes: 14 additions & 1 deletion src/core/reducer.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,19 @@ export function CreateGameReducer({ game, numPlayers, multiplayer }) {
return state;
}

// Ignore the event if the player isn't allowed to make it.
if (
action.payload.playerID !== null &&
action.payload.playerID !== undefined &&
!game.flow.canPlayerCallEvent(
state.G,
state.ctx,
action.payload.playerID
)
) {
return state;
}

// Initialize PRNG from ctx.
const random = new Random(state.ctx);
// Initialize Events API.
Expand Down Expand Up @@ -128,7 +141,7 @@ export function CreateGameReducer({ game, numPlayers, multiplayer }) {
return state;
}

// Ignore the move if the player cannot make it at this point.
// Ignore the move if the player isn't allowed to make it.
if (
action.payload.playerID !== null &&
action.payload.playerID !== undefined &&
Expand Down
4 changes: 4 additions & 0 deletions src/core/reducer.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ test('disable move by invalid playerIDs', () => {
state = reducer(state, makeMove('A', null, '1'));
expect(state._stateID).toBe(0);

// playerID="1" cannot call events right now.
state = reducer(state, gameEvent('endTurn', null, '1'));
expect(state._stateID).toBe(0);

// playerID="0" can move.
state = reducer(state, makeMove('A', null, '0'));
expect(state._stateID).toBe(1);
Expand Down
16 changes: 14 additions & 2 deletions src/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const Redux = require('redux');

import { DBFromEnv } from './db';
import { CreateGameReducer } from '../core/reducer';
import { MAKE_MOVE, GAME_EVENT } from '../core/action-types';
import { createApiServer, isActionFromAuthenticPlayer } from './api';

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

// Check whether the player is allowed to make the move
if (!game.flow.canPlayerMakeMove(state.G, state.ctx, playerID)) {
// Check whether the player is allowed to make the move.
if (
action.type == MAKE_MOVE &&
!game.flow.canPlayerMakeMove(state.G, state.ctx, playerID)
) {
return;
}

// Check whether the player is allowed to call the event.
if (
action.type == GAME_EVENT &&
!game.flow.canPlayerCallEvent(state.G, state.ctx, playerID)
) {
return;
}

Expand Down
76 changes: 43 additions & 33 deletions src/server/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -255,52 +255,62 @@ test('action', async () => {
// ... and not if player != currentPlayer
await io.socket.receive('action', action, 1, 'gameID', '100');
expect(io.socket.emit).toHaveBeenCalledTimes(0);
await io.socket.receive(
'action',
ActionCreators.makeMove(),
1,
'gameID',
'100'
);
expect(io.socket.emit).toHaveBeenCalledTimes(0);

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

test('playerView (sync)', async () => {
// Write the player into G.
const game = Game({
playerView: (G, ctx, player) => {
return Object.assign({}, G, { player });
},
});

const server = Server({ games: [game] });
const io = server.app.context.io;
describe('playerView', () => {
test('sync', async () => {
// Write the player into G.
const game = Game({
playerView: (G, ctx, player) => {
return Object.assign({}, G, { player });
},
});

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

test('playerView (action)', async () => {
const game = Game({
playerView: (G, ctx, player) => {
return Object.assign({}, G, { player });
},
await io.socket.receive('sync', 'gameID', 0);
expect(io.socket.emit).toHaveBeenCalledTimes(1);
expect(io.socket.emit.mock.calls[0][2].G).toEqual({ player: 0 });
});
const server = Server({ games: [game] });
const io = server.app.context.io;
const action = ActionCreators.gameEvent('endTurn');

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

await io.socket.receive('action', action, 0, 'gameID', '0');
expect(io.socket.emit).toHaveBeenCalledTimes(2);
io.socket.id = 'first';
await io.socket.receive('sync', 'gameID', '0', 2);
io.socket.id = 'second';
await io.socket.receive('sync', 'gameID', '1', 2);
io.socket.emit.mockReset();

await io.socket.receive('action', action, 0, 'gameID', '0');
expect(io.socket.emit).toHaveBeenCalledTimes(2);

const G_player0 = io.socket.emit.mock.calls[0][2].G;
const G_player1 = io.socket.emit.mock.calls[1][2].G;
const G_player0 = io.socket.emit.mock.calls[0][2].G;
const G_player1 = io.socket.emit.mock.calls[1][2].G;

expect(G_player0.player).toBe('0');
expect(G_player1.player).toBe('1');
expect(G_player0.player).toBe('0');
expect(G_player1.player).toBe('1');
});
});

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

1 comment on commit c4a11a7

@nicolodavis
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.