boardgame.io
Version:
library for turn-based games
1,347 lines (1,254 loc) • 39.6 kB
JavaScript
;
function _interopDefault (ex) { return (ex && (typeof ex === 'object') && 'default' in ex) ? ex['default'] : ex; }
var produce = _interopDefault(require('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 },
};
exports.ActionCreators = ActionCreators;
exports.ActivePlayers = ActivePlayers;
exports.Enhance = Enhance;
exports.EnhanceCtx = EnhanceCtx;
exports.Flush = Flush;
exports.FnWrap = FnWrap;
exports.GAME_EVENT = GAME_EVENT;
exports.INVALID_MOVE = INVALID_MOVE;
exports.InitTurnOrderState = InitTurnOrderState;
exports.MAKE_MOVE = MAKE_MOVE;
exports.NoClient = NoClient;
exports.PLUGIN = PLUGIN;
exports.ProcessAction = ProcessAction;
exports.REDO = REDO;
exports.RESET = RESET;
exports.SYNC = SYNC;
exports.SetActivePlayers = SetActivePlayers;
exports.SetActivePlayersEvent = SetActivePlayersEvent;
exports.Setup = Setup;
exports.Stage = Stage;
exports.TurnOrder = TurnOrder;
exports.UNDO = UNDO;
exports.UPDATE = UPDATE;
exports.UpdateActivePlayersOnceEmpty = UpdateActivePlayersOnceEmpty;
exports.UpdateTurnOrderState = UpdateTurnOrderState;
exports._assertThisInitialized = _assertThisInitialized;
exports._classCallCheck = _classCallCheck;
exports._createClass = _createClass;
exports._defineProperty = _defineProperty;
exports._getPrototypeOf = _getPrototypeOf;
exports._inherits = _inherits;
exports._objectSpread2 = _objectSpread2;
exports._objectWithoutProperties = _objectWithoutProperties;
exports._possibleConstructorReturn = _possibleConstructorReturn;
exports._toConsumableArray = _toConsumableArray;
exports.alea = alea;
exports.error = error;
exports.gameEvent = gameEvent;
exports.info = info;
exports.makeMove = makeMove;
exports.redo = redo;
exports.reset = reset;
exports.sync = sync;
exports.undo = undo;
exports.update = update;