boardgame.io
Version:
library for turn-based games
1,299 lines (1,208 loc) • 38.7 kB
JavaScript
import produce from 'immer';
/*
* 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.
* @param {string} playerID - The ID of the player making this action.
* @param {string} credentials - (optional) The credentials for the player making this action.
*/
const undo = (playerID, credentials) => ({
type: UNDO,
payload: { type: null, args: null, playerID, credentials },
});
/**
* Used to redo the last undone move.
* @param {string} playerID - The ID of the player making this action.
* @param {string} credentials - (optional) The credentials for the player making this action.
*/
const redo = (playerID, credentials) => ({
type: REDO,
payload: { type: null, args: null, playerID, credentials },
});
/**
* 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
});
/**
* Moves can return this when they want to indicate
* that the combination of arguments is illegal and
* the move ought to be discarded.
*/
const INVALID_MOVE = 'INVALID_MOVE';
/*
* 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.
*/
const ImmerPlugin = {
name: 'plugin-immer',
fnWrap: (move) => (G, ctx, ...args) => {
let isInvalid = false;
const newG = produce(G, G => {
const result = move(G, ctx, ...args);
if (result === INVALID_MOVE) {
isInvalid = true;
return;
}
return result;
});
if (isInvalid)
return INVALID_MOVE;
return newG;
},
};
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");
}
// 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.
*/
const RandomPlugin = {
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 };
},
};
/*
* 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.
*/
/**
* Events
*/
class Events {
constructor(flow, playerID) {
this.flow = flow;
this.playerID = playerID;
this.dispatch = [];
}
/**
* Attaches the Events API to ctx.
* @param {object} ctx - The ctx object to attach to.
*/
api(ctx) {
const events = {
_obj: this,
};
const { phase, turn } = ctx;
for (const key of this.flow.eventNames) {
events[key] = (...args) => {
this.dispatch.push({ key, args, phase, turn });
};
}
return events;
}
isUsed() {
return this.dispatch.length > 0;
}
/**
* Updates ctx with the triggered events.
* @param {object} state - The state object { G, ctx }.
*/
update(state) {
for (let i = 0; i < this.dispatch.length; i++) {
const 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;
}
const action = automaticGameEvent(item.key, item.args, this.playerID);
state = {
...state,
...this.flow.processEvent(state, action),
};
}
return state;
}
}
/*
* 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.
*/
const EventsPlugin = {
name: 'events',
noClient: ({ api }) => {
return api._obj.isUsed();
},
dangerouslyFlushRawState: ({ 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 = [ImmerPlugin, RandomPlugin, EventsPlugin];
/**
* 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);
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,
playerID: opts.playerID,
});
state = {
...state,
plugins: {
...state.plugins,
[name]: { ...pluginState, api },
},
};
});
return state;
};
/**
* Allows plugins to update their state after a move / event.
*/
const Flush = (state, opts) => {
// Note that we flush plugins in reverse order, to make sure that plugins
// that come before in the chain are still available.
[...DEFAULT_PLUGINS, ...opts.game.plugins].reverse().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.dangerouslyFlushRawState) {
state = plugin.dangerouslyFlushRawState({
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.
*/
const production = process.env.NODE_ENV === 'production';
const logfn = production ? () => { } : console.log;
const errorfn = console.error;
function info(msg) {
logfn(`INFO: ${msg}`);
}
function error(error) {
errorfn('ERROR:', error);
}
/*
* 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.
*/
/**
* Event to change the active players (and their stages) in the current turn.
*/
function SetActivePlayersEvent(state, _playerID, arg) {
return { ...state, ctx: SetActivePlayers(state.ctx, arg) };
}
function SetActivePlayers(ctx, arg) {
let { _prevActivePlayers } = ctx;
let activePlayers = {};
let _nextActivePlayers = null;
let _activePlayersMoveLimit = {};
if (Array.isArray(arg)) {
// support a simple array of player IDs as active players
let value = {};
arg.forEach(v => (value[v] = Stage.NULL));
activePlayers = value;
}
else {
// process active players argument object
if (arg.next) {
_nextActivePlayers = arg.next;
}
if (arg.revert) {
_prevActivePlayers = _prevActivePlayers.concat({
activePlayers: ctx.activePlayers,
_activePlayersMoveLimit: ctx._activePlayersMoveLimit,
_activePlayersNumMoves: ctx._activePlayersNumMoves,
});
}
else {
_prevActivePlayers = [];
}
if (arg.currentPlayer !== undefined) {
ApplyActivePlayerArgument(activePlayers, _activePlayersMoveLimit, ctx.currentPlayer, arg.currentPlayer);
}
if (arg.others !== undefined) {
for (let i = 0; i < ctx.playOrder.length; i++) {
const id = ctx.playOrder[i];
if (id !== ctx.currentPlayer) {
ApplyActivePlayerArgument(activePlayers, _activePlayersMoveLimit, id, arg.others);
}
}
}
if (arg.all !== undefined) {
for (let i = 0; i < ctx.playOrder.length; i++) {
const id = ctx.playOrder[i];
ApplyActivePlayerArgument(activePlayers, _activePlayersMoveLimit, id, arg.all);
}
}
if (arg.value) {
for (const id in arg.value) {
ApplyActivePlayerArgument(activePlayers, _activePlayersMoveLimit, id, arg.value[id]);
}
}
if (arg.moveLimit) {
for (const id in activePlayers) {
if (_activePlayersMoveLimit[id] === undefined) {
_activePlayersMoveLimit[id] = arg.moveLimit;
}
}
}
}
if (Object.keys(activePlayers).length == 0) {
activePlayers = null;
}
if (Object.keys(_activePlayersMoveLimit).length == 0) {
_activePlayersMoveLimit = null;
}
let _activePlayersNumMoves = {};
for (const id in activePlayers) {
_activePlayersNumMoves[id] = 0;
}
return {
...ctx,
activePlayers,
_activePlayersMoveLimit,
_activePlayersNumMoves,
_prevActivePlayers,
_nextActivePlayers,
};
}
/**
* Update activePlayers, setting it to previous, next or null values
* when it becomes empty.
* @param ctx
*/
function UpdateActivePlayersOnceEmpty(ctx) {
let { activePlayers, _activePlayersMoveLimit, _activePlayersNumMoves, _prevActivePlayers, } = ctx;
if (activePlayers && Object.keys(activePlayers).length == 0) {
if (ctx._nextActivePlayers) {
ctx = SetActivePlayers(ctx, ctx._nextActivePlayers);
({
activePlayers,
_activePlayersMoveLimit,
_activePlayersNumMoves,
_prevActivePlayers,
} = ctx);
}
else if (_prevActivePlayers.length > 0) {
const lastIndex = _prevActivePlayers.length - 1;
({
activePlayers,
_activePlayersMoveLimit,
_activePlayersNumMoves,
} = _prevActivePlayers[lastIndex]);
_prevActivePlayers = _prevActivePlayers.slice(0, lastIndex);
}
else {
activePlayers = null;
_activePlayersMoveLimit = null;
}
}
return {
...ctx,
activePlayers,
_activePlayersMoveLimit,
_activePlayersNumMoves,
_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) {
// convert to string in case playOrder is set to number[]
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.
*/
function InitTurnOrderState(state, turn) {
let { G, ctx } = state;
const ctxWithAPI = EnhanceCtx(state);
const order = turn.order;
let playOrder = [...new Array(ctx.numPlayers)].map((_, i) => i + '');
if (order.playOrder !== undefined) {
playOrder = order.playOrder(G, ctxWithAPI);
}
const playOrderPos = order.first(G, ctxWithAPI);
const posType = typeof playOrderPos;
if (posType !== 'number') {
error(`invalid value returned by turn.order.first — expected number got ${posType} “${playOrderPos}”.`);
}
const currentPlayer = getCurrentPlayer(playOrder, playOrderPos);
ctx = { ...ctx, currentPlayer, playOrderPos, 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(state, currentPlayer, turn, endTurnArg) {
const order = turn.order;
let { G, ctx } = state;
let playOrderPos = ctx.playOrderPos;
let endPhase = false;
if (endTurnArg && endTurnArg !== true) {
if (typeof endTurnArg !== 'object') {
error(`invalid argument to endTurn: ${endTurnArg}`);
}
Object.keys(endTurnArg).forEach(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: ${arg}`);
}
});
}
else {
const ctxWithAPI = EnhanceCtx(state);
const t = order.next(G, ctxWithAPI);
const type = typeof t;
if (t !== undefined && type !== 'number') {
error(`invalid value returned by turn.order.next — expected number or undefined got ${type} “${t}”.`);
}
if (t === undefined) {
endPhase = true;
}
else {
playOrderPos = t;
currentPlayer = getCurrentPlayer(ctx.playOrder, playOrderPos);
}
}
ctx = {
...ctx,
playOrderPos,
currentPlayer,
};
return { endPhase, 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.
*/
const TurnOrder = {
/**
* DEFAULT
*
* The default round-robin turn order.
*/
DEFAULT: {
first: (G, ctx) => ctx.turn === 0
? ctx.playOrderPos
: (ctx.playOrderPos + 1) % ctx.playOrder.length,
next: (G, ctx) => (ctx.playOrderPos + 1) % ctx.playOrder.length,
},
/**
* RESET
*
* Similar to DEFAULT, but starts from 0 each time.
*/
RESET: {
first: () => 0,
next: (G, ctx) => (ctx.playOrderPos + 1) % ctx.playOrder.length,
},
/**
* CONTINUE
*
* Similar to DEFAULT, but starts with the player who ended the last phase.
*/
CONTINUE: {
first: (G, ctx) => ctx.playOrderPos,
next: (G, ctx) => (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: () => 0,
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: (playOrder) => ({
playOrder: () => playOrder,
first: () => 0,
next: (G, ctx) => (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: (playOrderField) => ({
playOrder: (G) => G[playOrderField],
first: () => 0,
next: (G, ctx) => (ctx.playOrderPos + 1) % ctx.playOrder.length,
}),
};
const Stage = {
NULL: null,
};
const 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 },
};
export { ActionCreators as A, _getPrototypeOf as B, _assertThisInitialized as C, _toConsumableArray as D, EnhanceCtx as E, FnWrap as F, GAME_EVENT as G, update as H, InitTurnOrderState as I, _objectWithoutProperties as J, _objectSpread2 as K, MAKE_MOVE as M, NoClient as N, PLUGIN as P, RESET as R, Stage as S, TurnOrder as T, UpdateTurnOrderState as U, _createClass as _, SetActivePlayersEvent as a, SetActivePlayers as b, UpdateActivePlayersOnceEmpty as c, Enhance as d, error as e, Flush as f, gameEvent as g, INVALID_MOVE as h, info as i, UPDATE as j, SYNC as k, UNDO as l, REDO as m, ProcessAction as n, makeMove as o, alea as p, Setup as q, reset as r, sync as s, redo as t, undo as u, ActivePlayers as v, _classCallCheck as w, _inherits as x, _defineProperty as y, _possibleConstructorReturn as z };