UNPKG

boardgame.io

Version:
1,776 lines (1,622 loc) 71.5 kB
'use strict'; function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; } var produce = _interopDefault(require('immer')); function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function (obj) { return typeof obj; }; } else { _typeof = function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; } function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); keys.push.apply(keys, symbols); } return keys; } function _objectSpread2(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(source, true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(source).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; } function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function"); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, writable: true, configurable: true } }); if (superClass) _setPrototypeOf(subClass, superClass); } function _getPrototypeOf(o) { _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) { return o.__proto__ || Object.getPrototypeOf(o); }; return _getPrototypeOf(o); } function _setPrototypeOf(o, p) { _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) { o.__proto__ = p; return o; }; return _setPrototypeOf(o, p); } function _objectWithoutPropertiesLoose(source, excluded) { if (source == null) return {}; var target = {}; var sourceKeys = Object.keys(source); var key, i; for (i = 0; i < sourceKeys.length; i++) { key = sourceKeys[i]; if (excluded.indexOf(key) >= 0) continue; target[key] = source[key]; } return target; } function _objectWithoutProperties(source, excluded) { if (source == null) return {}; var target = _objectWithoutPropertiesLoose(source, excluded); var key, i; if (Object.getOwnPropertySymbols) { var sourceSymbolKeys = Object.getOwnPropertySymbols(source); for (i = 0; i < sourceSymbolKeys.length; i++) { key = sourceSymbolKeys[i]; if (excluded.indexOf(key) >= 0) continue; if (!Object.prototype.propertyIsEnumerable.call(source, key)) continue; target[key] = source[key]; } } return target; } function _assertThisInitialized(self) { if (self === void 0) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return self; } function _possibleConstructorReturn(self, call) { if (call && (typeof call === "object" || typeof call === "function")) { return call; } return _assertThisInitialized(self); } function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread(); } function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) arr2[i] = arr[i]; return arr2; } } function _iterableToArray(iter) { if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter); } function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance"); } /* * 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. */ const MAKE_MOVE = 'MAKE_MOVE'; const GAME_EVENT = 'GAME_EVENT'; const REDO = 'REDO'; const RESET = 'RESET'; const SYNC = 'SYNC'; const UNDO = 'UNDO'; const UPDATE = 'UPDATE'; const PLUGIN = 'PLUGIN'; /* * 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. */ /** * Generate a move to be dispatched to the game move reducer. * * @param {string} type - The move type. * @param {Array} args - Additional arguments. * @param {string} playerID - The ID of the player making this action. * @param {string} credentials - (optional) The credentials for the player making this action. */ const makeMove = (type, args, playerID, credentials) => ({ type: MAKE_MOVE, payload: { type, args, playerID, credentials }, }); /** * Generate a game event to be dispatched to the flow reducer. * * @param {string} type - The event type. * @param {Array} args - Additional arguments. * @param {string} playerID - The ID of the player making this action. * @param {string} credentials - (optional) The credentials for the player making this action. */ const gameEvent = (type, args, playerID, credentials) => ({ type: GAME_EVENT, payload: { type, args, playerID, credentials }, }); /** * Generate an automatic game event that is a side-effect of a move. * @param {string} type - The event type. * @param {Array} args - Additional arguments. * @param {string} playerID - The ID of the player making this action. * @param {string} credentials - (optional) The credentials for the player making this action. */ const automaticGameEvent = (type, args, playerID, credentials) => ({ type: GAME_EVENT, payload: { type, args, playerID, credentials }, automatic: true, }); const sync = (info) => ({ type: SYNC, state: info.state, log: info.log, initialState: info.initialState, clientOnly: true, }); /** * Used to update the Redux store's state in response to * an action coming from another player. * @param {object} state - The state to restore. * @param {Array} deltalog - A log delta. */ const update = (state, deltalog) => ({ type: UPDATE, state, deltalog, clientOnly: true, }); /** * Used to reset the game state. * @param {object} state - The initial state. */ const reset = (state) => ({ type: RESET, state, clientOnly: true, }); /** * Used to undo the last move. */ const undo = () => ({ type: UNDO, }); /** * Used to redo the last undone move. */ const redo = () => ({ type: REDO, }); /** * Allows plugins to define their own actions and intercept them. */ const plugin = (type, args, playerID, credentials) => ({ type: PLUGIN, payload: { type, args, playerID, credentials }, }); var ActionCreators = /*#__PURE__*/Object.freeze({ makeMove: makeMove, gameEvent: gameEvent, automaticGameEvent: automaticGameEvent, sync: sync, update: update, reset: reset, undo: undo, redo: redo, plugin: plugin }); /* * Copyright 2018 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. */ /** * Plugin that allows using Immer to make immutable changes * to G by just mutating it. */ var PluginImmer = { name: 'plugin-immer', fnWrap: function fnWrap(move) { return produce(move); } }; // Inlined version of Alea from https://github.com/davidbau/seedrandom. /* * Copyright 2015 David Bau. * * Permission is hereby granted, free of charge, * to any person obtaining a copy of this software * and associated documentation files (the "Software"), * to deal in the Software without restriction, including * without limitation the rights to use, copy, modify, merge, * publish, distribute, sublicense, and/or sell copies of the * Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall * be included in all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER * DEALINGS IN THE SOFTWARE. */ function Alea(seed) { var me = this, mash = Mash(); me.next = function () { var t = 2091639 * me.s0 + me.c * 2.3283064365386963e-10; // 2^-32 me.s0 = me.s1; me.s1 = me.s2; return me.s2 = t - (me.c = t | 0); }; // Apply the seeding algorithm from Baagoe. me.c = 1; me.s0 = mash(' '); me.s1 = mash(' '); me.s2 = mash(' '); me.s0 -= mash(seed); if (me.s0 < 0) { me.s0 += 1; } me.s1 -= mash(seed); if (me.s1 < 0) { me.s1 += 1; } me.s2 -= mash(seed); if (me.s2 < 0) { me.s2 += 1; } mash = null; } function copy(f, t) { t.c = f.c; t.s0 = f.s0; t.s1 = f.s1; t.s2 = f.s2; return t; } function Mash() { var n = 0xefc8249d; var mash = function mash(data) { data = data.toString(); for (var i = 0; i < data.length; i++) { n += data.charCodeAt(i); var h = 0.02519603282416938 * n; n = h >>> 0; h -= n; h *= n; n = h >>> 0; h -= n; n += h * 0x100000000; // 2^32 } return (n >>> 0) * 2.3283064365386963e-10; // 2^-32 }; return mash; } function alea(seed, opts) { var xg = new Alea(seed), state = opts && opts.state, prng = xg.next; prng.quick = prng; if (state) { if (_typeof(state) == 'object') copy(state, xg); prng.state = function () { return copy(xg, {}); }; } return prng; } /** * Random * * Calls that require a pseudorandom number generator. * Uses a seed from ctx, and also persists the PRNG * state in ctx so that moves can stay pure. */ var Random = /*#__PURE__*/ function () { /** * constructor * @param {object} ctx - The ctx object to initialize from. */ function Random(state) { _classCallCheck(this, Random); // If we are on the client, the seed is not present. // Just use a temporary seed to execute the move without // crashing it. The move state itself is discarded, // so the actual value doesn't matter. this.state = state; this.used = false; } _createClass(Random, [{ key: "isUsed", value: function isUsed() { return this.used; } }, { key: "getState", value: function getState() { return this.state; } /** * Generate a random number. */ }, { key: "_random", value: function _random() { this.used = true; var R = this.state; var fn; if (R.prngstate === undefined) { // No call to a random function has been made. fn = new alea(R.seed, { state: true }); } else { fn = new alea('', { state: R.prngstate }); } var number = fn(); this.state = _objectSpread2({}, R, { prngstate: fn.state() }); return number; } }, { key: "api", value: function api() { var random = this._random.bind(this); var SpotValue = { D4: 4, D6: 6, D8: 8, D10: 10, D12: 12, D20: 20 }; // Generate functions for predefined dice values D4 - D20. var predefined = {}; var _loop = function _loop(key) { var spotvalue = SpotValue[key]; predefined[key] = function (diceCount) { if (diceCount === undefined) { return Math.floor(random() * spotvalue) + 1; } else { return _toConsumableArray(new Array(diceCount).keys()).map(function () { return Math.floor(random() * spotvalue) + 1; }); } }; }; for (var key in SpotValue) { _loop(key); } return _objectSpread2({}, predefined, { /** * Roll a die of specified spot value. * * @param {number} spotvalue - The die dimension (default: 6). * @param {number} diceCount - number of dice to throw. * if not defined, defaults to 1 and returns the value directly. * if defined, returns an array containing the random dice values. */ Die: function Die(spotvalue, diceCount) { if (spotvalue === undefined) { spotvalue = 6; } if (diceCount === undefined) { return Math.floor(random() * spotvalue) + 1; } else { return _toConsumableArray(new Array(diceCount).keys()).map(function () { return Math.floor(random() * spotvalue) + 1; }); } }, /** * Generate a random number between 0 and 1. */ Number: function Number() { return random(); }, /** * Shuffle an array. * * @param {Array} deck - The array to shuffle. Does not mutate * the input, but returns the shuffled array. */ Shuffle: function Shuffle(deck) { var clone = deck.slice(0); var srcIndex = deck.length; var dstIndex = 0; var shuffled = new Array(srcIndex); while (srcIndex) { var randIndex = srcIndex * random() | 0; shuffled[dstIndex++] = clone[randIndex]; clone[randIndex] = clone[--srcIndex]; } return shuffled; }, _obj: this }); } }]); return Random; }(); /** * Generates a new seed from the current date / time. */ Random.seed = function () { return (+new Date()).toString(36).slice(-10); }; /* * Copyright 2018 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. */ var PluginRandom = { name: 'random', noClient: ({ api }) => { return api._obj.isUsed(); }, flush: ({ api }) => { return api._obj.getState(); }, api: ({ data }) => { const random = new Random(data); return random.api(); }, setup: ({ game }) => { let seed = game.seed; if (seed === undefined) { seed = Random.seed(); } return { seed }; }, }; /** * Events */ var Events = /*#__PURE__*/ function () { function Events(flow, playerID) { _classCallCheck(this, Events); this.flow = flow; this.playerID = playerID; this.dispatch = []; } /** * Attaches the Events API to ctx. * @param {object} ctx - The ctx object to attach to. */ _createClass(Events, [{ key: "api", value: function api(ctx) { var _this = this; var events = {}; var phase = ctx.phase, turn = ctx.turn; var _iteratorNormalCompletion = true; var _didIteratorError = false; var _iteratorError = undefined; try { var _loop = function _loop() { var key = _step.value; events[key] = function () { for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } _this.dispatch.push({ key: key, args: args, phase: phase, turn: turn }); }; }; for (var _iterator = this.flow.eventNames[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { _loop(); } } catch (err) { _didIteratorError = true; _iteratorError = err; } finally { try { if (!_iteratorNormalCompletion && _iterator["return"] != null) { _iterator["return"](); } } finally { if (_didIteratorError) { throw _iteratorError; } } } events._obj = this; return events; } }, { key: "isUsed", value: function isUsed() { return this.dispatch.length > 0; } /** * Updates ctx with the triggered events. * @param {object} state - The state object { G, ctx }. */ }, { key: "update", value: function update(state) { for (var i = 0; i < this.dispatch.length; i++) { var item = this.dispatch[i]; // If the turn already ended some other way, // don't try to end the turn again. if (item.key === 'endTurn' && item.turn !== state.ctx.turn) { continue; } // If the phase already ended some other way, // don't try to end the phase again. if ((item.key === 'endPhase' || item.key === 'setPhase') && item.phase !== state.ctx.phase) { continue; } var action = automaticGameEvent(item.key, item.args, this.playerID); state = _objectSpread2({}, state, {}, this.flow.processEvent(state, action)); } return state; } }]); return Events; }(); /* * Copyright 2020 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. */ var PluginEvents = { name: 'events', noClient: ({ api }) => { return api._obj.isUsed(); }, flushRaw: ({ state, api }) => { return api._obj.update(state); }, api: ({ game, playerID, ctx }) => { return new Events(game.flow, playerID).api(ctx); }, }; /* * Copyright 2018 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. */ /** * List of plugins that are always added. */ const DEFAULT_PLUGINS = [PluginImmer, PluginRandom, PluginEvents]; /** * Allow plugins to intercept actions and process them. */ const ProcessAction = (state, action, opts) => { opts.game.plugins .filter(plugin => plugin.action !== undefined) .filter(plugin => plugin.name === action.payload.type) .forEach(plugin => { const name = plugin.name; const pluginState = state.plugins[name] || { data: {} }; const data = plugin.action(pluginState.data, action.payload); state = { ...state, plugins: { ...state.plugins, [name]: { ...pluginState, data }, }, }; }); return state; }; /** * The API's created by various plugins are stored in the plugins * section of the state object: * * { * G: {}, * ctx: {}, * plugins: { * plugin-a: { * data: {}, // this is generated by the plugin at Setup / Flush. * api: {}, // this is ephemeral and generated by Enhance. * } * } * } * * This function takes these API's and stuffs them back into * ctx for consumption inside a move function or hook. */ const EnhanceCtx = (state) => { let ctx = { ...state.ctx }; const plugins = state.plugins || {}; Object.entries(plugins).forEach(([name, { api }]) => { ctx[name] = api; }); return ctx; }; /** * Applies the provided plugins to the given move / flow function. * * @param {function} fn - The move function or trigger to apply the plugins to. * @param {object} plugins - The list of plugins. */ const FnWrap = (fn, plugins) => { const reducer = (acc, { fnWrap }) => fnWrap(acc, plugins); return [...DEFAULT_PLUGINS, ...plugins] .filter(plugin => plugin.fnWrap !== undefined) .reduce(reducer, fn); }; /** * Allows the plugin to generate its initial state. */ const Setup = (state, opts) => { [...DEFAULT_PLUGINS, ...opts.game.plugins] .filter(plugin => plugin.setup !== undefined) .forEach(plugin => { const name = plugin.name; const data = plugin.setup({ G: state.G, ctx: state.ctx, game: opts.game, }); state = { ...state, plugins: { ...state.plugins, [name]: { data }, }, }; }); return state; }; /** * Invokes the plugin before a move or event. * The API that the plugin generates is stored inside * the `plugins` section of the state (which is subsequently * merged into ctx). */ const Enhance = (state, opts) => { [...DEFAULT_PLUGINS, ...opts.game.plugins] .filter(plugin => plugin.api !== undefined) .forEach(plugin => { const name = plugin.name; const pluginState = state.plugins[name] || { data: {} }; const api = plugin.api({ G: state.G, ctx: state.ctx, data: pluginState.data, game: opts.game, }); state = { ...state, plugins: { ...state.plugins, [name]: { ...pluginState, api }, }, }; }); return state; }; /** * Allows plugins to update their state after a move / event. */ const Flush = (state, opts) => { [...DEFAULT_PLUGINS, ...opts.game.plugins].forEach(plugin => { const name = plugin.name; const pluginState = state.plugins[name] || { data: {} }; if (plugin.flush) { const newData = plugin.flush({ G: state.G, ctx: state.ctx, game: opts.game, api: pluginState.api, data: pluginState.data, }); state = { ...state, plugins: { ...state.plugins, [plugin.name]: { data: newData }, }, }; } else if (plugin.flushRaw) { state = plugin.flushRaw({ state, game: opts.game, api: pluginState.api, data: pluginState.data, }); // Remove everything other than data. const data = state.plugins[name].data; state = { ...state, plugins: { ...state.plugins, [plugin.name]: { data }, }, }; } }); return state; }; /** * Allows plugins to indicate if they should not be materialized on the client. * This will cause the client to discard the state update and wait for the * master instead. */ const NoClient = (state, opts) => { return [...DEFAULT_PLUGINS, ...opts.game.plugins] .filter(plugin => plugin.noClient !== undefined) .map(plugin => { const name = plugin.name; const pluginState = state.plugins[name]; if (pluginState) { return plugin.noClient({ G: state.G, ctx: state.ctx, game: opts.game, api: pluginState.api, data: pluginState.data, }); } return false; }) .some(value => value === true); }; /* * Copyright 2018 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. */ var production = process.env.NODE_ENV === 'production'; var logfn = production ? function () {} : console.log; var errorfn = console.error; function info(msg) { logfn("INFO: ".concat(msg)); } function error(error) { errorfn('ERROR:', error); } /** * Event to change the active players (and their stages) in the current turn. */ function SetActivePlayersEvent(state, _playerID, arg) { return _objectSpread2({}, state, { ctx: SetActivePlayers(state.ctx, arg) }); } function SetActivePlayers(ctx, arg) { var _prevActivePlayers = ctx._prevActivePlayers; var _nextActivePlayers = arg.next || null; if (arg.revert) { _prevActivePlayers = _prevActivePlayers.concat({ activePlayers: ctx.activePlayers, _activePlayersMoveLimit: ctx._activePlayersMoveLimit, _activePlayersNumMoves: ctx._activePlayersNumMoves }); } else { _prevActivePlayers = []; } var activePlayers = {}; var _activePlayersMoveLimit = {}; if (Array.isArray(arg)) { var value = {}; arg.forEach(function (v) { return value[v] = Stage.NULL; }); activePlayers = value; } if (arg.currentPlayer !== undefined) { ApplyActivePlayerArgument(activePlayers, _activePlayersMoveLimit, ctx.currentPlayer, arg.currentPlayer); } if (arg.others !== undefined) { for (var i = 0; i < ctx.playOrder.length; i++) { var id = ctx.playOrder[i]; if (id !== ctx.currentPlayer) { ApplyActivePlayerArgument(activePlayers, _activePlayersMoveLimit, id, arg.others); } } } if (arg.all !== undefined) { for (var _i = 0; _i < ctx.playOrder.length; _i++) { var _id = ctx.playOrder[_i]; ApplyActivePlayerArgument(activePlayers, _activePlayersMoveLimit, _id, arg.all); } } if (arg.value) { for (var _id2 in arg.value) { ApplyActivePlayerArgument(activePlayers, _activePlayersMoveLimit, _id2, arg.value[_id2]); } } if (arg.moveLimit) { for (var _id3 in activePlayers) { if (_activePlayersMoveLimit[_id3] === undefined) { _activePlayersMoveLimit[_id3] = arg.moveLimit; } } } if (Object.keys(activePlayers).length == 0) { activePlayers = null; } if (Object.keys(_activePlayersMoveLimit).length == 0) { _activePlayersMoveLimit = null; } var _activePlayersNumMoves = {}; for (var _id4 in activePlayers) { _activePlayersNumMoves[_id4] = 0; } return _objectSpread2({}, ctx, { activePlayers: activePlayers, _activePlayersMoveLimit: _activePlayersMoveLimit, _activePlayersNumMoves: _activePlayersNumMoves, _prevActivePlayers: _prevActivePlayers, _nextActivePlayers: _nextActivePlayers }); } /** * Update activePlayers, setting it to previous, next or null values * when it becomes empty. * @param {Object} ctx */ function UpdateActivePlayersOnceEmpty(ctx) { var _ctx = ctx, activePlayers = _ctx.activePlayers, _activePlayersMoveLimit = _ctx._activePlayersMoveLimit, _activePlayersNumMoves = _ctx._activePlayersNumMoves, _prevActivePlayers = _ctx._prevActivePlayers; if (activePlayers && Object.keys(activePlayers).length == 0) { if (ctx._nextActivePlayers) { ctx = SetActivePlayers(ctx, ctx._nextActivePlayers); var _ctx2 = ctx; activePlayers = _ctx2.activePlayers; _activePlayersMoveLimit = _ctx2._activePlayersMoveLimit; _activePlayersNumMoves = _ctx2._activePlayersNumMoves; _prevActivePlayers = _ctx2._prevActivePlayers; } else if (_prevActivePlayers.length > 0) { var lastIndex = _prevActivePlayers.length - 1; var _prevActivePlayers$la = _prevActivePlayers[lastIndex]; activePlayers = _prevActivePlayers$la.activePlayers; _activePlayersMoveLimit = _prevActivePlayers$la._activePlayersMoveLimit; _activePlayersNumMoves = _prevActivePlayers$la._activePlayersNumMoves; _prevActivePlayers = _prevActivePlayers.slice(0, lastIndex); } else { activePlayers = null; _activePlayersMoveLimit = null; } } return _objectSpread2({}, ctx, { activePlayers: activePlayers, _activePlayersMoveLimit: _activePlayersMoveLimit, _activePlayersNumMoves: _activePlayersNumMoves, _prevActivePlayers: _prevActivePlayers }); } /** * Apply an active player argument to the given player ID * @param {Object} activePlayers * @param {Object} _activePlayersMoveLimit * @param {String} playerID The player to apply the parameter to * @param {(String|Object)} arg An active player argument */ function ApplyActivePlayerArgument(activePlayers, _activePlayersMoveLimit, playerID, arg) { if (_typeof(arg) !== 'object' || arg === Stage.NULL) { arg = { stage: arg }; } if (arg.stage !== undefined) { activePlayers[playerID] = arg.stage; if (arg.moveLimit) _activePlayersMoveLimit[playerID] = arg.moveLimit; } } /** * Converts a playOrderPos index into its value in playOrder. * @param {Array} playOrder - An array of player ID's. * @param {number} playOrderPos - An index into the above. */ function getCurrentPlayer(playOrder, playOrderPos) { return playOrder[playOrderPos] + ''; } /** * Called at the start of a turn to initialize turn order state. * * TODO: This is called inside StartTurn, which is called from * both UpdateTurn and StartPhase (so it's called at the beginning * of a new phase as well as between turns). We should probably * split it into two. * * @param {object} G - The game object G. * @param {object} ctx - The game object ctx. * @param {object} turn - A turn object for this phase. */ function InitTurnOrderState(G, ctx, turn) { var order = turn.order; var playOrder = _toConsumableArray(new Array(ctx.numPlayers)).map(function (d, i) { return i + ''; }); if (order.playOrder !== undefined) { playOrder = order.playOrder(G, ctx); } var playOrderPos = order.first(G, ctx); var currentPlayer = getCurrentPlayer(playOrder, playOrderPos); ctx = _objectSpread2({}, ctx, { currentPlayer: currentPlayer, playOrderPos: playOrderPos, playOrder: playOrder }); ctx = SetActivePlayers(ctx, turn.activePlayers || {}); return ctx; } /** * Called at the end of each turn to update the turn order state. * @param {object} G - The game object G. * @param {object} ctx - The game object ctx. * @param {object} turn - A turn object for this phase. * @param {string} endTurnArg - An optional argument to endTurn that may specify the next player. */ function UpdateTurnOrderState(G, ctx, turn, endTurnArg) { var order = turn.order; var playOrderPos = ctx.playOrderPos; var currentPlayer = ctx.currentPlayer; var endPhase = false; if (endTurnArg && endTurnArg !== true) { if (_typeof(endTurnArg) !== 'object') { error("invalid argument to endTurn: ".concat(endTurnArg)); } Object.keys(endTurnArg).forEach(function (arg) { switch (arg) { case 'remove': currentPlayer = getCurrentPlayer(ctx.playOrder, playOrderPos); break; case 'next': playOrderPos = ctx.playOrder.indexOf(endTurnArg.next); currentPlayer = endTurnArg.next; break; default: error("invalid argument to endTurn: ".concat(arg)); } }); } else { var t = order.next(G, ctx); if (t === undefined) { endPhase = true; } else { playOrderPos = t; currentPlayer = getCurrentPlayer(ctx.playOrder, playOrderPos); } } ctx = _objectSpread2({}, ctx, { playOrderPos: playOrderPos, currentPlayer: currentPlayer }); return { endPhase: endPhase, ctx: ctx }; } /** * Set of different turn orders possible in a phase. * These are meant to be passed to the `turn` setting * in the flow objects. * * Each object defines the first player when the phase / game * begins, and also a function `next` to determine who the * next player is when the turn ends. * * The phase ends if next() returns undefined. */ var TurnOrder = { /** * DEFAULT * * The default round-robin turn order. */ DEFAULT: { first: function first(G, ctx) { return ctx.turn === 0 ? ctx.playOrderPos : (ctx.playOrderPos + 1) % ctx.playOrder.length; }, next: function next(G, ctx) { return (ctx.playOrderPos + 1) % ctx.playOrder.length; } }, /** * RESET * * Similar to DEFAULT, but starts from 0 each time. */ RESET: { first: function first() { return 0; }, next: function next(G, ctx) { return (ctx.playOrderPos + 1) % ctx.playOrder.length; } }, /** * CONTINUE * * Similar to DEFAULT, but starts with the player who ended the last phase. */ CONTINUE: { first: function first(G, ctx) { return ctx.playOrderPos; }, next: function next(G, ctx) { return (ctx.playOrderPos + 1) % ctx.playOrder.length; } }, /** * ONCE * * Another round-robin turn order, but goes around just once. * The phase ends after all players have played. */ ONCE: { first: function first() { return 0; }, next: function next(G, ctx) { if (ctx.playOrderPos < ctx.playOrder.length - 1) { return ctx.playOrderPos + 1; } } }, /** * CUSTOM * * Identical to DEFAULT, but also sets playOrder at the * beginning of the phase. * * @param {Array} playOrder - The play order. */ CUSTOM: function CUSTOM(_playOrder) { return { playOrder: function playOrder() { return _playOrder; }, first: function first() { return 0; }, next: function next(G, ctx) { return (ctx.playOrderPos + 1) % ctx.playOrder.length; } }; }, /** * CUSTOM_FROM * * Identical to DEFAULT, but also sets playOrder at the * beginning of the phase to a value specified by a field * in G. * * @param {string} playOrderField - Field in G. */ CUSTOM_FROM: function CUSTOM_FROM(playOrderField) { return { playOrder: function playOrder(G) { return G[playOrderField]; }, first: function first() { return 0; }, next: function next(G, ctx) { return (ctx.playOrderPos + 1) % ctx.playOrder.length; } }; } }; var Stage = { NULL: null }; var ActivePlayers = { /** * ALL * * The turn stays with one player, but any player can play (in any order) * until the phase ends. */ ALL: { all: Stage.NULL }, /** * ALL_ONCE * * The turn stays with one player, but any player can play (once, and in any order). * This is typically used in a phase where you want to elicit a response * from every player in the game. */ ALL_ONCE: { all: Stage.NULL, moveLimit: 1 }, /** * OTHERS * * The turn stays with one player, and every *other* player can play (in any order) * until the phase ends. */ OTHERS: { others: Stage.NULL }, /** * OTHERS_ONCE * * The turn stays with one player, and every *other* player can play (once, and in any order). * This is typically used in a phase where you want to elicit a response * from every *other* player in the game. */ OTHERS_ONCE: { others: Stage.NULL, moveLimit: 1 } }; /* * 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, }) { // 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) { 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 = FnWrap(fn, plugins); return (state) => { const ctxWithAPI = EnhanceCtx(state); return withPlugins(state.G, ctxWithAPI); }; }; const TriggerWrapper = (endIf) => { return (state) => { let ctxWithAPI = 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.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 = SetActivePlayers(ctx, conf.turn.activePlayers); } } else { // This is only called at the beginning of the phase // when there is no currentPlayer yet. ctx = InitTurnOrderState(G, ctx, conf.turn); } const turn = ctx.turn + 1; ctx = { ...ctx, turn, numMoves: 0, _prevActivePlayers: [] }; G = conf.turn.wrapped.onBegin({ ...state, G, ctx }); const _undo = [{ 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 { 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 } = UpdateTurnOrderState(G, { ...ctx, 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 = 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) { 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.