UNPKG

boardgame.io

Version:
302 lines (254 loc) 7.83 kB
/* * Copyright 2017 The boardgame.io Authors * * Use of this source code is governed by a MIT-style * license that can be found in the LICENSE file or at * https://opensource.org/licenses/MIT. */ import * as Actions from './action-types'; import * as plugins from '../plugins/main'; import { ProcessGameConfig } from './game'; import { error } from './logger'; import { INVALID_MOVE } from './constants'; import { ActionShape, Ctx, Game, LogEntry, State, Move, LongFormMove, } from '../types'; /** * Returns true if a move can be undone. */ const CanUndoMove = (G: any, ctx: Ctx, move: Move): boolean => { function HasUndoable(move: Move): move is LongFormMove { return (move as LongFormMove).undoable !== undefined; } function IsFunction( undoable: boolean | ((...args: any[]) => any) ): undoable is (...args: any[]) => any { return undoable instanceof Function; } if (!HasUndoable(move)) { return true; } if (IsFunction(move.undoable)) { return move.undoable(G, ctx); } return move.undoable; }; /** * CreateGameReducer * * Creates the main game state reducer. */ export function CreateGameReducer({ game, isClient, }: { game: Game; isClient?: boolean; }) { game = ProcessGameConfig(game); /** * GameReducer * * Redux reducer that maintains the overall game state. * @param {object} state - The state before the action. * @param {object} action - A Redux action. */ return (state: State | null = null, action: ActionShape.Any): State => { switch (action.type) { case Actions.GAME_EVENT: { state = { ...state, deltalog: [] }; // Process game events only on the server. // These events like `endTurn` typically // contain code that may rely on secret state // and cannot be computed on the client. if (isClient) { return state; } // Disallow events once the game is over. if (state.ctx.gameover !== undefined) { error(`cannot call event after game end`); return state; } // Ignore the event if the player isn't active. if ( action.payload.playerID !== null && action.payload.playerID !== undefined && !game.flow.isPlayerActive(state.G, state.ctx, action.payload.playerID) ) { error(`disallowed event: ${action.payload.type}`); return state; } // Execute plugins. state = plugins.Enhance(state, { game, isClient: false, playerID: action.payload.playerID, }); // Process event. let newState = game.flow.processEvent(state, action); // Execute plugins. newState = plugins.Flush(newState, { game, isClient: false }); return { ...newState, _stateID: state._stateID + 1 }; } case Actions.MAKE_MOVE: { state = { ...state, deltalog: [] }; // Check whether the move is allowed at this time. const move: Move = game.flow.getMove( state.ctx, action.payload.type, action.payload.playerID || state.ctx.currentPlayer ); if (move === null) { error(`disallowed move: ${action.payload.type}`); return state; } // Don't run move on client if move says so. if (isClient && (move as LongFormMove).client === false) { return state; } // Disallow moves once the game is over. if (state.ctx.gameover !== undefined) { error(`cannot make move after game end`); return state; } // Ignore the move if the player isn't active. if ( action.payload.playerID !== null && action.payload.playerID !== undefined && !game.flow.isPlayerActive(state.G, state.ctx, action.payload.playerID) ) { error(`disallowed move: ${action.payload.type}`); return state; } // Execute plugins. state = plugins.Enhance(state, { game, isClient, playerID: action.payload.playerID, }); // Process the move. let G = game.processMove(state, action.payload); // The game declared the move as invalid. if (G === INVALID_MOVE) { error( `invalid move: ${action.payload.type} args: ${action.payload.args}` ); return state; } // Create a log entry for this move. let logEntry: LogEntry = { action, _stateID: state._stateID, turn: state.ctx.turn, phase: state.ctx.phase, }; if ((move as LongFormMove).redact === true) { logEntry.redact = true; } const newState = { ...state, G, deltalog: [logEntry], _stateID: state._stateID + 1, }; // Some plugin indicated that it is not suitable to be // materialized on the client (and must wait for the server // response instead). if (isClient && plugins.NoClient(newState, { game })) { return state; } state = newState; // If we're on the client, just process the move // and no triggers in multiplayer mode. // These will be processed on the server, which // will send back a state update. if (isClient) { state = plugins.Flush(state, { game, isClient: true, }); return state; } const prevTurnCount = state.ctx.turn; // Allow the flow reducer to process any triggers that happen after moves. state = game.flow.processMove(state, action.payload); state = plugins.Flush(state, { game }); // Update undo / redo state. // Only update undo stack if the turn has not been ended if (state.ctx.turn === prevTurnCount && !game.disableUndo) { state._undo = state._undo.concat({ G: state.G, ctx: state.ctx, moveType: action.payload.type, }); } // Always reset redo stack when making a move state._redo = []; return state; } case Actions.RESET: case Actions.UPDATE: case Actions.SYNC: { return action.state; } case Actions.UNDO: { if (game.disableUndo) { error("Undo is not enabled"); return state; } const { _undo, _redo } = state; if (_undo.length < 2) { return state; } const last = _undo[_undo.length - 1]; const restore = _undo[_undo.length - 2]; // Only allow undoable moves to be undone. const lastMove: Move = game.flow.getMove( restore.ctx, last.moveType, action.payload.playerID ); if (!CanUndoMove(state.G, state.ctx, lastMove)) { return state; } return { ...state, G: restore.G, ctx: restore.ctx, _undo: _undo.slice(0, _undo.length - 1), _redo: [last, ..._redo], }; } case Actions.REDO: { const { _undo, _redo } = state; if (game.disableUndo) { error("Redo is not enabled"); return state; } if (_redo.length == 0) { return state; } const first = _redo[0]; return { ...state, G: first.G, ctx: first.ctx, _undo: [..._undo, first], _redo: _redo.slice(1), }; } case Actions.PLUGIN: { return plugins.ProcessAction(state, action, { game }); } default: { return state; } } }; }