UNPKG

boardgame.io

Version:
968 lines (962 loc) 35.1 kB
'use strict'; var turnOrder = require('./turn-order-d6c2e620.js'); /* * 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. */ /** * Flow * * Creates a reducer that updates ctx (analogous to how moves update G). */ function Flow({ moves, phases, endIf, onEnd, turn, events, plugins, disableUndo, }) { // Attach defaults. if (moves === undefined) { moves = {}; } if (events === undefined) { events = {}; } if (plugins === undefined) { plugins = []; } if (phases === undefined) { phases = {}; } if (!endIf) endIf = () => undefined; if (!onEnd) onEnd = G => G; if (!turn) turn = {}; const phaseMap = { ...phases }; if ('' in phaseMap) { turnOrder.error('cannot specify phase with empty name'); } phaseMap[''] = {}; let moveMap = {}; let moveNames = new Set(); let startingPhase = null; Object.keys(moves).forEach(name => moveNames.add(name)); const HookWrapper = (fn) => { const withPlugins = turnOrder.FnWrap(fn, plugins); return (state) => { const ctxWithAPI = turnOrder.EnhanceCtx(state); return withPlugins(state.G, ctxWithAPI); }; }; const TriggerWrapper = (endIf) => { return (state) => { let ctxWithAPI = turnOrder.EnhanceCtx(state); return endIf(state.G, ctxWithAPI); }; }; const wrapped = { onEnd: HookWrapper(onEnd), endIf: TriggerWrapper(endIf), }; for (let phase in phaseMap) { const conf = phaseMap[phase]; if (conf.start === true) { startingPhase = phase; } if (conf.moves !== undefined) { for (let move of Object.keys(conf.moves)) { moveMap[phase + '.' + move] = conf.moves[move]; moveNames.add(move); } } if (conf.endIf === undefined) { conf.endIf = () => undefined; } if (conf.onBegin === undefined) { conf.onBegin = G => G; } if (conf.onEnd === undefined) { conf.onEnd = G => G; } if (conf.turn === undefined) { conf.turn = turn; } if (conf.turn.order === undefined) { conf.turn.order = turnOrder.TurnOrder.DEFAULT; } if (conf.turn.onBegin === undefined) { conf.turn.onBegin = G => G; } if (conf.turn.onEnd === undefined) { conf.turn.onEnd = G => G; } if (conf.turn.endIf === undefined) { conf.turn.endIf = () => false; } if (conf.turn.onMove === undefined) { conf.turn.onMove = G => G; } if (conf.turn.stages === undefined) { conf.turn.stages = {}; } for (const stage in conf.turn.stages) { const stageConfig = conf.turn.stages[stage]; const moves = stageConfig.moves || {}; for (let move of Object.keys(moves)) { let key = phase + '.' + stage + '.' + move; moveMap[key] = moves[move]; moveNames.add(move); } } conf.wrapped = { onBegin: HookWrapper(conf.onBegin), onEnd: HookWrapper(conf.onEnd), endIf: TriggerWrapper(conf.endIf), }; conf.turn.wrapped = { onMove: HookWrapper(conf.turn.onMove), onBegin: HookWrapper(conf.turn.onBegin), onEnd: HookWrapper(conf.turn.onEnd), endIf: TriggerWrapper(conf.turn.endIf), }; } function GetPhase(ctx) { return ctx.phase ? phaseMap[ctx.phase] : phaseMap['']; } function OnMove(s) { return s; } function Process(state, events) { const phasesEnded = new Set(); const turnsEnded = new Set(); for (let i = 0; i < events.length; i++) { const { fn, arg, ...rest } = events[i]; // Detect a loop of EndPhase calls. // This could potentially even be an infinite loop // if the endIf condition of each phase blindly // returns true. The moment we detect a single // loop, we just bail out of all phases. if (fn === EndPhase) { turnsEnded.clear(); const phase = state.ctx.phase; if (phasesEnded.has(phase)) { const ctx = { ...state.ctx, phase: null }; return { ...state, ctx }; } phasesEnded.add(phase); } // Process event. let next = []; state = fn(state, { ...rest, arg, next, }); if (fn === EndGame) { break; } // Check if we should end the game. const shouldEndGame = ShouldEndGame(state); if (shouldEndGame) { events.push({ fn: EndGame, arg: shouldEndGame, turn: state.ctx.turn, phase: state.ctx.phase, automatic: true, }); continue; } // Check if we should end the phase. const shouldEndPhase = ShouldEndPhase(state); if (shouldEndPhase) { events.push({ fn: EndPhase, arg: shouldEndPhase, turn: state.ctx.turn, phase: state.ctx.phase, automatic: true, }); continue; } // Check if we should end the turn. if (fn === OnMove) { const shouldEndTurn = ShouldEndTurn(state); if (shouldEndTurn) { events.push({ fn: EndTurn, arg: shouldEndTurn, turn: state.ctx.turn, phase: state.ctx.phase, automatic: true, }); continue; } } events.push(...next); } return state; } /////////// // Start // /////////// function StartGame(state, { next }) { next.push({ fn: StartPhase }); return state; } function StartPhase(state, { next }) { let { G, ctx } = state; const conf = GetPhase(ctx); // Run any phase setup code provided by the user. G = conf.wrapped.onBegin(state); next.push({ fn: StartTurn }); return { ...state, G, ctx }; } function StartTurn(state, { currentPlayer }) { let { G, ctx } = state; const conf = GetPhase(ctx); // Initialize the turn order state. if (currentPlayer) { ctx = { ...ctx, currentPlayer }; if (conf.turn.activePlayers) { ctx = turnOrder.SetActivePlayers(ctx, conf.turn.activePlayers); } } else { // This is only called at the beginning of the phase // when there is no currentPlayer yet. ctx = turnOrder.InitTurnOrderState(state, conf.turn); } const turn = ctx.turn + 1; ctx = { ...ctx, turn, numMoves: 0, _prevActivePlayers: [] }; G = conf.turn.wrapped.onBegin({ ...state, G, ctx }); const _undo = disableUndo ? [] : [{ G, ctx }]; return { ...state, G, ctx, _undo, _redo: [] }; } //////////// // Update // //////////// function UpdatePhase(state, { arg, next, phase }) { const conf = GetPhase({ phase }); let { ctx } = state; if (arg && arg.next) { if (arg.next in phaseMap) { ctx = { ...ctx, phase: arg.next }; } else { turnOrder.error('invalid phase: ' + arg.next); return state; } } else if (conf.next !== undefined) { ctx = { ...ctx, phase: conf.next }; } else { ctx = { ...ctx, phase: null }; } state = { ...state, ctx }; // Start the new phase. next.push({ fn: StartPhase }); return state; } function UpdateTurn(state, { arg, currentPlayer, next }) { let { G, ctx } = state; const conf = GetPhase(ctx); // Update turn order state. const { endPhase, ctx: newCtx } = turnOrder.UpdateTurnOrderState(state, currentPlayer, conf.turn, arg); ctx = newCtx; state = { ...state, G, ctx }; if (endPhase) { next.push({ fn: EndPhase, turn: ctx.turn, phase: ctx.phase }); } else { next.push({ fn: StartTurn, currentPlayer: ctx.currentPlayer }); } return state; } function UpdateStage(state, { arg, playerID }) { if (typeof arg === 'string') { arg = { stage: arg }; } let { ctx } = state; let { activePlayers, _activePlayersMoveLimit, _activePlayersNumMoves, } = ctx; if (arg.stage) { if (activePlayers === null) { activePlayers = {}; } activePlayers[playerID] = arg.stage; _activePlayersNumMoves[playerID] = 0; if (arg.moveLimit) { if (_activePlayersMoveLimit === null) { _activePlayersMoveLimit = {}; } _activePlayersMoveLimit[playerID] = arg.moveLimit; } } ctx = { ...ctx, activePlayers, _activePlayersMoveLimit, _activePlayersNumMoves, }; return { ...state, ctx }; } /////////////// // ShouldEnd // /////////////// function ShouldEndGame(state) { return wrapped.endIf(state); } function ShouldEndPhase(state) { const conf = GetPhase(state.ctx); return conf.wrapped.endIf(state); } function ShouldEndTurn(state) { const conf = GetPhase(state.ctx); // End the turn if the required number of moves has been made. const currentPlayerMoves = state.ctx.numMoves || 0; if (conf.turn.moveLimit && currentPlayerMoves >= conf.turn.moveLimit) { return true; } return conf.turn.wrapped.endIf(state); } ///////// // End // ///////// function EndGame(state, { arg, phase }) { state = EndPhase(state, { phase }); if (arg === undefined) { arg = true; } state = { ...state, ctx: { ...state.ctx, gameover: arg } }; // Run game end hook. const G = wrapped.onEnd(state); return { ...state, G }; } function EndPhase(state, { arg, next, turn, automatic }) { // End the turn first. state = EndTurn(state, { turn, force: true }); let G = state.G; let ctx = state.ctx; if (next) { next.push({ fn: UpdatePhase, arg, phase: ctx.phase }); } // If we aren't in a phase, there is nothing else to do. if (ctx.phase === null) { return state; } // Run any cleanup code for the phase that is about to end. const conf = GetPhase(ctx); G = conf.wrapped.onEnd(state); // Reset the phase. ctx = { ...ctx, phase: null }; // Add log entry. const action = turnOrder.gameEvent('endPhase', arg); const logEntry = { action, _stateID: state._stateID, turn: state.ctx.turn, phase: state.ctx.phase, }; if (automatic) { logEntry.automatic = true; } const deltalog = [...state.deltalog, logEntry]; return { ...state, G, ctx, deltalog }; } function EndTurn(state, { arg, next, turn, force, automatic, playerID }) { // This is not the turn that EndTurn was originally // called for. The turn was probably ended some other way. if (turn !== state.ctx.turn) { return state; } let { G, ctx } = state; const conf = GetPhase(ctx); // Prevent ending the turn if moveLimit hasn't been reached. const currentPlayerMoves = ctx.numMoves || 0; if (!force && conf.turn.moveLimit && currentPlayerMoves < conf.turn.moveLimit) { turnOrder.info(`cannot end turn before making ${conf.turn.moveLimit} moves`); return state; } // Run turn-end triggers. G = conf.turn.wrapped.onEnd(state); if (next) { next.push({ fn: UpdateTurn, arg, currentPlayer: ctx.currentPlayer }); } // Reset activePlayers. ctx = { ...ctx, activePlayers: null }; // Remove player from playerOrder if (arg && arg.remove) { playerID = playerID || ctx.currentPlayer; const playOrder = ctx.playOrder.filter(i => i != playerID); const playOrderPos = ctx.playOrderPos > playOrder.length - 1 ? 0 : ctx.playOrderPos; ctx = { ...ctx, playOrder, playOrderPos }; if (playOrder.length === 0) { next.push({ fn: EndPhase, turn: ctx.turn, phase: ctx.phase }); return state; } } // Add log entry. const action = turnOrder.gameEvent('endTurn', arg); const logEntry = { action, _stateID: state._stateID, turn: state.ctx.turn, phase: state.ctx.phase, }; if (automatic) { logEntry.automatic = true; } const deltalog = [...(state.deltalog || []), logEntry]; return { ...state, G, ctx, deltalog, _undo: [], _redo: [] }; } function EndStage(state, { arg, next, automatic, playerID }) { playerID = playerID || state.ctx.currentPlayer; let { ctx } = state; let { activePlayers, _activePlayersMoveLimit } = ctx; const playerInStage = activePlayers !== null && playerID in activePlayers; if (!arg && playerInStage) { const conf = GetPhase(ctx); const stage = conf.turn.stages[activePlayers[playerID]]; if (stage && stage.next) arg = stage.next; } if (next && arg) { next.push({ fn: UpdateStage, arg, playerID }); } // If player isn’t in a stage, there is nothing else to do. if (!playerInStage) return state; // Remove player from activePlayers. activePlayers = Object.keys(activePlayers) .filter(id => id !== playerID) .reduce((obj, key) => { obj[key] = activePlayers[key]; return obj; }, {}); if (_activePlayersMoveLimit) { // Remove player from _activePlayersMoveLimit. _activePlayersMoveLimit = Object.keys(_activePlayersMoveLimit) .filter(id => id !== playerID) .reduce((obj, key) => { obj[key] = _activePlayersMoveLimit[key]; return obj; }, {}); } ctx = turnOrder.UpdateActivePlayersOnceEmpty({ ...ctx, activePlayers, _activePlayersMoveLimit, }); // Add log entry. const action = turnOrder.gameEvent('endStage', arg); const logEntry = { action, _stateID: state._stateID, turn: state.ctx.turn, phase: state.ctx.phase, }; if (automatic) { logEntry.automatic = true; } const deltalog = [...(state.deltalog || []), logEntry]; return { ...state, ctx, deltalog }; } /** * Retrieves the relevant move that can be played by playerID. * * If ctx.activePlayers is set (i.e. one or more players are in some stage), * then it attempts to find the move inside the stages config for * that turn. If the stage for a player is '', then the player is * allowed to make a move (as determined by the phase config), but * isn't restricted to a particular set as defined in the stage config. * * If not, it then looks for the move inside the phase. * * If it doesn't find the move there, it looks at the global move definition. * * @param {object} ctx * @param {string} name * @param {string} playerID */ function GetMove(ctx, name, playerID) { const conf = GetPhase(ctx); const stages = conf.turn.stages; const { activePlayers } = ctx; if (activePlayers && activePlayers[playerID] !== undefined && activePlayers[playerID] !== turnOrder.Stage.NULL && stages[activePlayers[playerID]] !== undefined && stages[activePlayers[playerID]].moves !== undefined) { // Check if moves are defined for the player's stage. const stage = stages[activePlayers[playerID]]; const moves = stage.moves; if (name in moves) { return moves[name]; } } else if (conf.moves) { // Check if moves are defined for the current phase. if (name in conf.moves) { return conf.moves[name]; } } else if (name in moves) { // Check for the move globally. return moves[name]; } return null; } function ProcessMove(state, action) { let conf = GetPhase(state.ctx); const move = GetMove(state.ctx, action.type, action.playerID); const shouldCount = !move || typeof move === 'function' || move.noLimit !== true; let { ctx } = state; let { _activePlayersNumMoves } = ctx; const { playerID } = action; let numMoves = state.ctx.numMoves; if (shouldCount) { if (playerID == state.ctx.currentPlayer) { numMoves++; } if (ctx.activePlayers) _activePlayersNumMoves[playerID]++; } state = { ...state, ctx: { ...ctx, numMoves, _activePlayersNumMoves, }, }; if (ctx._activePlayersMoveLimit && _activePlayersNumMoves[playerID] >= ctx._activePlayersMoveLimit[playerID]) { state = EndStage(state, { playerID, automatic: true }); } const G = conf.turn.wrapped.onMove(state); state = { ...state, G }; let events = [{ fn: OnMove }]; return Process(state, events); } function SetStageEvent(state, playerID, arg) { return Process(state, [{ fn: EndStage, arg, playerID }]); } function EndStageEvent(state, playerID) { return Process(state, [{ fn: EndStage, playerID }]); } function SetPhaseEvent(state, _playerID, newPhase) { return Process(state, [ { fn: EndPhase, phase: state.ctx.phase, turn: state.ctx.turn, arg: { next: newPhase }, }, ]); } function EndPhaseEvent(state) { return Process(state, [ { fn: EndPhase, phase: state.ctx.phase, turn: state.ctx.turn }, ]); } function EndTurnEvent(state, _playerID, arg) { return Process(state, [ { fn: EndTurn, turn: state.ctx.turn, phase: state.ctx.phase, arg }, ]); } function PassEvent(state, _playerID, arg) { return Process(state, [ { fn: EndTurn, turn: state.ctx.turn, phase: state.ctx.phase, force: true, arg, }, ]); } function EndGameEvent(state, _playerID, arg) { return Process(state, [ { fn: EndGame, turn: state.ctx.turn, phase: state.ctx.phase, arg }, ]); } const eventHandlers = { endStage: EndStageEvent, setStage: SetStageEvent, endTurn: EndTurnEvent, pass: PassEvent, endPhase: EndPhaseEvent, setPhase: SetPhaseEvent, endGame: EndGameEvent, setActivePlayers: turnOrder.SetActivePlayersEvent, }; let enabledEventNames = []; if (events.endTurn !== false) { enabledEventNames.push('endTurn'); } if (events.pass !== false) { enabledEventNames.push('pass'); } if (events.endPhase !== false) { enabledEventNames.push('endPhase'); } if (events.setPhase !== false) { enabledEventNames.push('setPhase'); } if (events.endGame !== false) { enabledEventNames.push('endGame'); } if (events.setActivePlayers !== false) { enabledEventNames.push('setActivePlayers'); } if (events.endStage !== false) { enabledEventNames.push('endStage'); } if (events.setStage !== false) { enabledEventNames.push('setStage'); } function ProcessEvent(state, action) { const { type, playerID, args } = action.payload; if (eventHandlers.hasOwnProperty(type)) { const eventArgs = [state, playerID].concat(args); return eventHandlers[type].apply({}, eventArgs); } return state; } function IsPlayerActive(_G, ctx, playerID) { if (ctx.activePlayers) { return playerID in ctx.activePlayers; } return ctx.currentPlayer === playerID; } return { ctx: (numPlayers) => ({ numPlayers, turn: 0, currentPlayer: '0', playOrder: [...new Array(numPlayers)].map((_d, i) => i + ''), playOrderPos: 0, phase: startingPhase, activePlayers: null, }), init: (state) => { return Process(state, [{ fn: StartGame }]); }, isPlayerActive: IsPlayerActive, eventHandlers, eventNames: Object.keys(eventHandlers), enabledEventNames, moveMap, moveNames: [...moveNames.values()], processMove: ProcessMove, processEvent: ProcessEvent, getMove: GetMove, }; } /* * 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. */ function IsProcessed(game) { return game.processMove !== undefined; } /** * Helper to generate the game move reducer. The returned * reducer has the following signature: * * (G, action, ctx) => {} * * You can roll your own if you like, or use any Redux * addon to generate such a reducer. * * The convention used in this framework is to * have action.type contain the name of the move, and * action.args contain any additional arguments as an * Array. */ function ProcessGameConfig(game) { // The Game() function has already been called on this // config object, so just pass it through. if (IsProcessed(game)) { return game; } if (game.name === undefined) game.name = 'default'; if (game.disableUndo === undefined) game.disableUndo = false; if (game.setup === undefined) game.setup = () => ({}); if (game.moves === undefined) game.moves = {}; if (game.playerView === undefined) game.playerView = G => G; if (game.plugins === undefined) game.plugins = []; game.plugins.forEach(plugin => { if (plugin.name === undefined) { throw new Error('Plugin missing name attribute'); } if (plugin.name.includes(' ')) { throw new Error(plugin.name + ': Plugin name must not include spaces'); } }); if (game.name.includes(' ')) { throw new Error(game.name + ': Game name must not include spaces'); } const flow = Flow(game); return { ...game, flow, moveNames: flow.moveNames, pluginNames: game.plugins.map(p => p.name), processMove: (state, action) => { let moveFn = flow.getMove(state.ctx, action.type, action.playerID); if (IsLongFormMove(moveFn)) { moveFn = moveFn.move; } if (moveFn instanceof Function) { const fn = turnOrder.FnWrap(moveFn, game.plugins); const ctxWithAPI = { ...turnOrder.EnhanceCtx(state), playerID: action.playerID, }; let args = []; if (action.args !== undefined) { args = args.concat(action.args); } return fn(state.G, ctxWithAPI, ...args); } turnOrder.error(`invalid move object: ${action.type}`); return state.G; }, }; } function IsLongFormMove(move) { return move instanceof Object && move.move !== undefined; } /* * 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. */ /** * Returns true if a move can be undone. */ const CanUndoMove = (G, ctx, move) => { function HasUndoable(move) { return move.undoable !== undefined; } function IsFunction(undoable) { 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. */ function CreateGameReducer({ game, isClient, }) { 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 = null, action) => { switch (action.type) { case turnOrder.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) { turnOrder.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)) { turnOrder.error(`disallowed event: ${action.payload.type}`); return state; } // Execute plugins. state = turnOrder.Enhance(state, { game, isClient: false, playerID: action.payload.playerID, }); // Process event. let newState = game.flow.processEvent(state, action); // Execute plugins. newState = turnOrder.Flush(newState, { game, isClient: false }); return { ...newState, _stateID: state._stateID + 1 }; } case turnOrder.MAKE_MOVE: { state = { ...state, deltalog: [] }; // Check whether the move is allowed at this time. const move = game.flow.getMove(state.ctx, action.payload.type, action.payload.playerID || state.ctx.currentPlayer); if (move === null) { turnOrder.error(`disallowed move: ${action.payload.type}`); return state; } // Don't run move on client if move says so. if (isClient && move.client === false) { return state; } // Disallow moves once the game is over. if (state.ctx.gameover !== undefined) { turnOrder.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)) { turnOrder.error(`disallowed move: ${action.payload.type}`); return state; } // Execute plugins. state = turnOrder.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 === turnOrder.INVALID_MOVE) { turnOrder.error(`invalid move: ${action.payload.type} args: ${action.payload.args}`); return state; } // Create a log entry for this move. let logEntry = { action, _stateID: state._stateID, turn: state.ctx.turn, phase: state.ctx.phase, }; if (move.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 && turnOrder.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 = turnOrder.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 = turnOrder.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 turnOrder.RESET: case turnOrder.UPDATE: case turnOrder.SYNC: { return action.state; } case turnOrder.UNDO: { if (game.disableUndo) { turnOrder.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 = 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 turnOrder.REDO: { const { _undo, _redo } = state; if (game.disableUndo) { turnOrder.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 turnOrder.PLUGIN: { return turnOrder.ProcessAction(state, action, { game }); } default: { return state; } } }; } exports.CreateGameReducer = CreateGameReducer; exports.ProcessGameConfig = ProcessGameConfig;