diff --git a/src/client/client.js b/src/client/client.js index 5714ca388..c223586cc 100644 --- a/src/client/client.js +++ b/src/client/client.js @@ -21,12 +21,27 @@ function createDispatchers( innerActionNames, store, playerID, - credentials + credentials, + multiplayer ) { return innerActionNames.reduce((dispatchers, name) => { dispatchers[name] = function(...args) { + let assumedPlayerID = playerID; + + // In singleplayer mode, if the client does not have a playerID + // associated with it, we attach the currentPlayer as playerID. + if (!multiplayer && (playerID === null || playerID === undefined)) { + const state = store.getState(); + assumedPlayerID = state.ctx.currentPlayer; + } + store.dispatch( - ActionCreators[storeActionType](name, args, playerID, credentials) + ActionCreators[storeActionType]( + name, + args, + assumedPlayerID, + credentials + ) ); }; return dispatchers; @@ -209,14 +224,16 @@ class _ClientImpl { this.game.moveNames, this.store, this.playerID, - this.credentials + this.credentials, + this.multiplayer ); this.events = createEventDispatchers( this.game.flow.eventNames, this.store, this.playerID, - this.credentials + this.credentials, + this.multiplayer ); } diff --git a/src/client/client.test.js b/src/client/client.test.js index 0e9278706..e30b9f660 100644 --- a/src/client/client.test.js +++ b/src/client/client.test.js @@ -13,6 +13,7 @@ import { createEventDispatchers, createMoveDispatchers, } from './client'; +import { gameEvent } from '../core/action-creators'; import Game from '../core/game'; import { RandomBot } from '../ai/bot'; @@ -197,32 +198,73 @@ test('event dispatchers', () => { } }); -test('move dispatchers', () => { +describe('move dispatchers', () => { const game = Game({ moves: { A: G => G, - B: () => ({ moved: true }), + B: (G, ctx) => ({ moved: ctx.playerID }), C: () => ({ victory: true }), }, flow: { endGameIf: (G, ctx) => (G.victory ? ctx.currentPlayer : undefined), }, }); - const reducer = CreateGameReducer({ game }); - const store = createStore(reducer); - const api = createMoveDispatchers(game.moveNames, store); - expect(Object.getOwnPropertyNames(api)).toEqual(['A', 'B', 'C']); - expect(api.unknown).toBe(undefined); + test('basic', () => { + const store = createStore(reducer); + const api = createMoveDispatchers(game.moveNames, store); + + expect(Object.getOwnPropertyNames(api)).toEqual(['A', 'B', 'C']); + expect(api.unknown).toBe(undefined); + + api.A(); + expect(store.getState().G).not.toMatchObject({ moved: true }); + expect(store.getState().G).not.toMatchObject({ victory: true }); + + api.B(); + expect(store.getState().G).toMatchObject({ moved: '0' }); + + store.dispatch(gameEvent('endTurn', null, '0')); + + api.B(); + expect(store.getState().G).toMatchObject({ moved: '1' }); + + api.C(); + expect(store.getState().G).toMatchObject({ victory: true }); + }); + + test('with undefined playerID - singleplayer mode', () => { + const store = createStore(reducer); + const api = createMoveDispatchers(game.moveNames, store); + api.B(); + expect(store.getState().G).toMatchObject({ moved: '0' }); + }); - api.A(); - expect(store.getState().G).not.toMatchObject({ moved: true }); - expect(store.getState().G).not.toMatchObject({ victory: true }); + test('with undefined playerID - multiplayer mode', () => { + const store = createStore(reducer); + const api = createMoveDispatchers( + game.moveNames, + store, + undefined, + null, + true + ); + api.B(); + expect(store.getState().G).toMatchObject({ moved: undefined }); + }); - api.B(); - expect(store.getState().G).toMatchObject({ moved: true }); + test('with null playerID - singleplayer mode', () => { + const store = createStore(reducer); + const api = createMoveDispatchers(game.moveNames, store, null); + api.B(); + expect(store.getState().G).toMatchObject({ moved: '0' }); + }); - api.C(); - expect(store.getState().G).toMatchObject({ victory: true }); + test('with null playerID - multiplayer mode', () => { + const store = createStore(reducer); + const api = createMoveDispatchers(game.moveNames, store, null, null, true); + api.B(); + expect(store.getState().G).toMatchObject({ moved: null }); + }); }); diff --git a/src/core/flow.js b/src/core/flow.js index 82b3778e0..d0ecf2d23 100644 --- a/src/core/flow.js +++ b/src/core/flow.js @@ -512,15 +512,15 @@ export function FlowWithPhases({ const currentPlayerMoves = state.ctx.currentPlayerMoves + 1; let _played = state.ctx._played; - if (action.payload && !_played.includes(action.payload.playerID)) { - _played = [..._played, action.payload.playerID]; + if (!_played.includes(action.playerID)) { + _played = [..._played, action.playerID]; } const allPlayed = _played.length == state.ctx.numPlayers; // Update actionPlayers if _actionPlayersOnce is set. let actionPlayers = state.ctx.actionPlayers; if (state.ctx._actionPlayersOnce == true) { - const playerID = action.payload.playerID; + const playerID = action.playerID; actionPlayers = actionPlayers.filter(id => id !== playerID); } @@ -574,7 +574,7 @@ export function FlowWithPhases({ // Update undo / redo state. if (!endTurn) { const undo = state._undo || []; - const moveType = action.payload.type; + const moveType = action.type; let plainCtx = state.ctx; plainCtx = Random.detach(plainCtx); diff --git a/src/core/flow.test.js b/src/core/flow.test.js index c949b828e..b5400043a 100644 --- a/src/core/flow.test.js +++ b/src/core/flow.test.js @@ -56,11 +56,11 @@ test('movesPerTurn', () => { let flow = FlowWithPhases({ movesPerTurn: 2 }); let state = { ctx: flow.ctx(2) }; expect(state.ctx.turn).toBe(0); - state = flow.processMove(state, { move: {}, payload: {} }); + state = flow.processMove(state, makeMove().payload); expect(state.ctx.turn).toBe(0); state = flow.processGameEvent(state, gameEvent('endTurn')); expect(state.ctx.turn).toBe(0); - state = flow.processMove(state, { move: {}, payload: {} }); + state = flow.processMove(state, makeMove().payload); expect(state.ctx.turn).toBe(1); } @@ -71,17 +71,17 @@ test('movesPerTurn', () => { }); let state = { ctx: flow.ctx(2) }; expect(state.ctx.turn).toBe(0); - state = flow.processMove(state, { move: {}, payload: {} }); + state = flow.processMove(state, makeMove().payload); expect(state.ctx.turn).toBe(0); state = flow.processGameEvent(state, gameEvent('endTurn')); expect(state.ctx.turn).toBe(0); - state = flow.processMove(state, { move: {}, payload: {} }); + state = flow.processMove(state, makeMove().payload); expect(state.ctx.turn).toBe(1); state = flow.processGameEvent(state, gameEvent('endPhase')); expect(state.ctx.turn).toBe(1); - state = flow.processMove(state, { move: {}, payload: {} }); + state = flow.processMove(state, makeMove().payload); expect(state.ctx.turn).toBe(2); } }); @@ -169,7 +169,7 @@ test('onMove', () => { { let flow = FlowWithPhases({ onMove }); let state = { G: {}, ctx: flow.ctx(2) }; - state = flow.processMove(state, { payload: {} }); + state = flow.processMove(state, makeMove().payload); expect(state.G).toEqual({ A: true }); } @@ -179,10 +179,10 @@ test('onMove', () => { phases: [{ name: 'A' }, { name: 'B', onMove: () => ({ B: true }) }], }); let state = { G: {}, ctx: flow.ctx(2) }; - state = flow.processMove(state, { payload: {} }); + state = flow.processMove(state, makeMove().payload); expect(state.G).toEqual({ A: true }); state = flow.processGameEvent(state, gameEvent('endPhase')); - state = flow.processMove(state, { payload: {} }); + state = flow.processMove(state, makeMove().payload); expect(state.G).toEqual({ B: true }); } }); @@ -254,7 +254,7 @@ test('endPhaseIf', () => { } { - const t = flow.processMove(state, { type: 'move', payload: {} }); + const t = flow.processMove(state, makeMove().payload); expect(t.ctx.phase).toBe('B'); } @@ -290,7 +290,7 @@ test('endGameIf', () => { } { - const t = flow.processMove(state, { type: 'move' }); + const t = flow.processMove(state, makeMove('move').payload); expect(t.ctx.gameover).toBe('A'); } } @@ -317,7 +317,7 @@ test('endGameIf', () => { } { - const t = flow.processMove(state, { type: 'move' }); + const t = flow.processMove(state, makeMove('move').payload); expect(t.ctx.gameover).toBe('A'); } } @@ -697,7 +697,7 @@ test('endPhaseOnMove', () => { let state = { G: {}, ctx: flow.ctx(2) }; expect(state.ctx.phase).toBe('A'); - state = flow.processMove(state, { payload: {} }); + state = flow.processMove(state, makeMove().payload); expect(state.ctx.phase).toBe('B'); expect(endPhaseACount).toEqual(1); diff --git a/src/core/reducer.js b/src/core/reducer.js index 7be2679bb..cb1663269 100644 --- a/src/core/reducer.js +++ b/src/core/reducer.js @@ -201,7 +201,7 @@ export function CreateGameReducer({ game, numPlayers, multiplayer }) { // Allow the flow reducer to process any triggers that happen after moves. state = { ...state, ctx: random.attach(state.ctx) }; state = { ...state, ctx: events.attach(state.ctx) }; - state = game.flow.processMove(state, action); + state = game.flow.processMove(state, action.payload); state = events.update(state); state = { ...state, ctx: random.update(state.ctx) }; state = { ...state, ctx: Random.detach(state.ctx) };