diff --git a/docs/turn-order.md b/docs/turn-order.md index ac4205290..dc0418f3a 100644 --- a/docs/turn-order.md +++ b/docs/turn-order.md @@ -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 diff --git a/src/core/flow.js b/src/core/flow.js index 8f9d9e5a4..99a815c00 100644 --- a/src/core/flow.js +++ b/src/core/flow.js @@ -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'); diff --git a/src/core/reducer.js b/src/core/reducer.js index e22e7766b..80ab1d2bb 100644 --- a/src/core/reducer.js +++ b/src/core/reducer.js @@ -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. @@ -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 && diff --git a/src/core/reducer.test.js b/src/core/reducer.test.js index 73e2be1d5..7fa43adf3 100644 --- a/src/core/reducer.test.js +++ b/src/core/reducer.test.js @@ -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); diff --git a/src/server/index.js b/src/server/index.js index 771c473d2..fb06864be 100644 --- a/src/server/index.js +++ b/src/server/index.js @@ -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; @@ -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; } diff --git a/src/server/index.test.js b/src/server/index.test.js index 232b482cc..9842ce90c 100644 --- a/src/server/index.test.js +++ b/src/server/index.test.js @@ -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 () => {