@freeboardgame.org/boardgame.io
Version:
library for turn-based games
1,699 lines (1,478 loc) • 75.8 kB
JavaScript
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('immer'), require('flatted')) :
typeof define === 'function' && define.amd ? define(['exports', 'immer', 'flatted'], factory) :
(global = global || self, factory(global.Core = {}, global.immer, global.Flatted));
}(this, function (exports, produce, flatted) { 'use strict';
produce = produce && produce.hasOwnProperty('default') ? produce['default'] : produce;
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 _objectSpread(target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i] != null ? arguments[i] : {};
var ownKeys = Object.keys(source);
if (typeof Object.getOwnPropertySymbols === 'function') {
ownKeys = ownKeys.concat(Object.getOwnPropertySymbols(source).filter(function (sym) {
return Object.getOwnPropertyDescriptor(source, sym).enumerable;
}));
}
ownKeys.forEach(function (key) {
_defineProperty(target, key, source[key]);
});
}
return target;
}
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 _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 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 = {
fnWrap: function fnWrap(move) {
return produce(move);
}
};
/**
* List of plugins that are always added.
*/
var DEFAULT_PLUGINS = [PluginImmer];
/**
* Applies the provided plugins to ctx before processing a move / event.
*
* @param {object} ctx - The ctx object.
* @param {object} game - The game object.
*/
var CtxPreMove = function CtxPreMove(ctx, game) {
[].concat(DEFAULT_PLUGINS, _toConsumableArray(game.plugins)).filter(function (plugin) {
return plugin.ctx !== undefined;
}).filter(function (plugin) {
return plugin.ctx.preMove !== undefined;
}).forEach(function (plugin) {
ctx = plugin.ctx.preMove(ctx, game);
});
return ctx;
};
/**
* Applies the provided plugins to G before processing a move / event.
*
* @param {object} G - The G object.
* @param {object} game - The game object.
*/
var GPreMove = function GPreMove(G, game) {
[].concat(DEFAULT_PLUGINS, _toConsumableArray(game.plugins)).filter(function (plugin) {
return plugin.G !== undefined;
}).filter(function (plugin) {
return plugin.G.preMove !== undefined;
}).forEach(function (plugin) {
G = plugin.G.preMove(G, game);
});
return G;
};
/**
* Postprocesses G after a move / event.
*
* @param {object} G - The G object.
* @param {object} game - The game object.
*/
var GPostMove = function GPostMove(G, game) {
[].concat(DEFAULT_PLUGINS, _toConsumableArray(game.plugins)).filter(function (plugin) {
return plugin.G !== undefined;
}).filter(function (plugin) {
return plugin.G.postMove !== undefined;
}).forEach(function (plugin) {
G = plugin.G.postMove(G, game);
});
return G;
};
/**
* 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} game - The game object.
*/
var FnWrap = function FnWrap(fn, game) {
var reducer = function reducer(acc, _ref) {
var fnWrap = _ref.fnWrap;
return fnWrap(acc, game);
};
var g = [].concat(DEFAULT_PLUGINS, _toConsumableArray(game.plugins)).filter(function (plugin) {
return plugin.fnWrap !== undefined;
}).reduce(reducer, fn);
return function (G, ctx) {
G = GPreMove(G, game);
ctx = CtxPreMove(ctx, game);
for (var _len = arguments.length, args = new Array(_len > 2 ? _len - 2 : 0), _key = 2; _key < _len; _key++) {
args[_key - 2] = arguments[_key];
}
G = g.apply(void 0, [G, ctx].concat(args));
G = GPostMove(G, game);
return G;
};
};
var G = {
/**
* Applies the provided plugins to G during game setup.
*
* @param {object} G - The G object.
* @param {object} ctx - The ctx object.
* @param {object} game - The game object.
*/
setup: function setup(G, ctx, game) {
[].concat(DEFAULT_PLUGINS, _toConsumableArray(game.plugins)).filter(function (plugin) {
return plugin.G !== undefined;
}).filter(function (plugin) {
return plugin.G.setup !== undefined;
}).forEach(function (plugin) {
G = plugin.G.setup(G, ctx, game);
});
return G;
},
/**
* Applies the provided plugins to G during the beginning of the phase.
*
* @param {object} G - The G object.
* @param {object} ctx - The ctx object.
* @param {object} game - The game object.
*/
onPhaseBegin: function onPhaseBegin(G, ctx, game) {
[].concat(DEFAULT_PLUGINS, _toConsumableArray(game.plugins)).filter(function (plugin) {
return plugin.G !== undefined;
}).filter(function (plugin) {
return plugin.G.onPhaseBegin !== undefined;
}).forEach(function (plugin) {
G = plugin.G.onPhaseBegin(G, ctx, game);
});
return G;
}
};
var ctx = {
/**
* Applies the provided plugins to ctx during game setup.
*
* @param {object} ctx - The ctx object.
* @param {object} game - The game object.
*/
setup: function setup(ctx, game) {
[].concat(DEFAULT_PLUGINS, _toConsumableArray(game.plugins)).filter(function (plugin) {
return plugin.ctx !== undefined;
}).filter(function (plugin) {
return plugin.ctx.setup !== undefined;
}).forEach(function (plugin) {
ctx = plugin.ctx.setup(ctx, game);
});
return ctx;
},
/**
* Applies the provided plugins to ctx during the beginning of the phase.
*
* @param {object} ctx - The ctx object.
* @param {object} game - The game object.
*/
onPhaseBegin: function onPhaseBegin(ctx, game) {
[].concat(DEFAULT_PLUGINS, _toConsumableArray(game.plugins)).filter(function (plugin) {
return plugin.ctx !== undefined;
}).filter(function (plugin) {
return plugin.ctx.onPhaseBegin !== undefined;
}).forEach(function (plugin) {
ctx = plugin.ctx.onPhaseBegin(ctx, game);
});
return 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.
*/
var DEV = process.env.NODE_ENV === 'development' || process.env.NODE_ENV == 'test';
var logfn = DEV ? console.log : function () {};
var errorfn = DEV ? console.error : function () {};
function error(error) {
errorfn('ERROR:', error);
}
/**
* Standard move that simulates passing.
*
* Creates two objects in G:
* passOrder - An array of playerIDs capturing passes in the pass order.
* allPassed - Set to true when all players have passed.
*/
var Pass = function Pass(G, ctx) {
var passOrder = [];
if (G.passOrder !== undefined) {
passOrder = G.passOrder;
}
var playerID = ctx.playerID;
passOrder = [].concat(_toConsumableArray(passOrder), [playerID]);
G = _objectSpread({}, G, {
passOrder: passOrder
});
if (passOrder.length >= ctx.numPlayers) {
G = _objectSpread({}, G, {
allPassed: true
});
}
return G;
};
/**
* Event to change the actionPlayers array.
* @param {object} state - The game state.
* @param {object} arg - An array of playerID's or <object> of:
* {
* value: (G, ctx) => [], // function that returns an array of playerID's (optional if all is set)
*
* all: true, // set value to all playerID's
*
* others: true, // set value to all except currentPlayer.
*
* once: true, // players have one move
* // (after which they're pruned from actionPlayers).
* // The phase ends once actionPlayers becomes empty.
* }
*/
function SetActionPlayersEvent(state, arg) {
return _objectSpread({}, state, {
ctx: setActionPlayers(state.G, state.ctx, arg)
});
}
function setActionPlayers(G, ctx, arg) {
var actionPlayers = [];
if (arg.value) {
actionPlayers = arg.value(G, ctx);
}
if (arg.all) {
actionPlayers = _toConsumableArray(ctx.playOrder);
}
if (arg.others) {
actionPlayers = _toConsumableArray(ctx.playOrder).filter(function (nr) {
return nr !== ctx.currentPlayer;
});
}
if (Array.isArray(arg)) {
actionPlayers = arg;
}
return _objectSpread({}, ctx, {
actionPlayers: actionPlayers,
_actionPlayersOnce: arg.once || false
});
}
/**
* 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 phase to initialize turn order state.
* @param {object} G - The game object G.
* @param {object} ctx - The game object ctx.
* @param {object} turnOrder - A turn order object for this phase.
*/
function InitTurnOrderState(G, ctx, turnOrder) {
var playOrder = _toConsumableArray(new Array(ctx.numPlayers)).map(function (d, i) {
return i + '';
});
if (turnOrder.playOrder !== undefined) {
playOrder = turnOrder.playOrder(G, ctx);
}
var playOrderPos = turnOrder.first(G, ctx);
var currentPlayer = getCurrentPlayer(playOrder, playOrderPos);
if (turnOrder.actionPlayers !== undefined) {
ctx = setActionPlayers(G, ctx, turnOrder.actionPlayers);
} else {
ctx = _objectSpread({}, ctx, {
actionPlayers: [currentPlayer]
});
}
return _objectSpread({}, ctx, {
currentPlayer: currentPlayer,
playOrderPos: playOrderPos,
playOrder: playOrder
});
}
/**
* 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} turnOrder - A turn order object for this phase.
* @param {string} endTurnArg - An optional argument to endTurn that
may specify the next player.
*/
function UpdateTurnOrderState(G, ctx, turnOrder, endTurnArg) {
var playOrderPos = ctx.playOrderPos;
var currentPlayer = ctx.currentPlayer;
var actionPlayers = ctx.actionPlayers;
var endPhase = false;
if (endTurnArg && endTurnArg !== true) {
if (ctx.playOrder.includes(endTurnArg.next)) {
playOrderPos = ctx.playOrder.indexOf(endTurnArg.next);
currentPlayer = endTurnArg.next;
actionPlayers = [currentPlayer];
} else {
error("invalid argument to endTurn: ".concat(endTurnArg));
}
} else {
var t = turnOrder.next(G, ctx);
if (t === undefined) {
endPhase = true;
} else {
playOrderPos = t;
currentPlayer = getCurrentPlayer(ctx.playOrder, playOrderPos);
if (turnOrder.actionPlayers === undefined) {
actionPlayers = [currentPlayer];
}
}
}
ctx = _objectSpread({}, ctx, {
playOrderPos: playOrderPos,
currentPlayer: currentPlayer,
actionPlayers: actionPlayers
});
return {
endPhase: endPhase,
ctx: ctx
};
}
/**
* Set of different turn orders possible in a phase.
* These are meant to be passed to the `turnOrder` 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.
*
* Objects can also contain an actionPlayers section which
* is passed to SetActionPlayers above at the beginning of
* the phase.
*
* The phase ends if next() returns undefined.
*/
var TurnOrder = {
/**
* DEFAULT
*
* The default round-robin turn order.
*/
DEFAULT: {
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;
}
}
},
/**
* ANY
*
* The turn stays with one player, but any player can play (in any order)
* until the phase ends.
*/
ANY: {
first: function first(G, ctx) {
return ctx.playOrderPos;
},
next: function next(G, ctx) {
return ctx.playOrderPos;
},
actionPlayers: {
all: true
}
},
/**
* ANY_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.
*/
ANY_ONCE: {
first: function first(G, ctx) {
return ctx.playOrderPos;
},
next: function next(G, ctx) {
return ctx.playOrderPos;
},
actionPlayers: {
all: true,
once: true
},
endPhaseOnceDone: true
},
/**
* OTHERS
*
* The turn stays with one player, and every *other* player can play (in any order)
* until the phase ends.
*/
OTHERS: {
first: function first(G, ctx) {
return ctx.playOrderPos;
},
next: function next(G, ctx) {
return ctx.playOrderPos;
},
actionPlayers: {
others: true
}
},
/**
* 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: {
first: function first(G, ctx) {
return ctx.playOrderPos;
},
next: function next(G, ctx) {
return ctx.playOrderPos;
},
actionPlayers: {
others: true,
once: true
},
endPhaseOnceDone: true
},
/**
* 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;
}
};
},
/**
* SKIP
*
* Round-robin, but skips over any players that have passed.
* Meant to be used with Pass above.
*/
SKIP: {
first: function first(G, ctx) {
return ctx.playOrderPos;
},
next: function next(G, ctx) {
if (G.allPassed) return;
var playOrderPos = ctx.playOrderPos;
for (var i = 0; i < ctx.playOrder.length; i++) {
playOrderPos = (playOrderPos + 1) % ctx.playOrder.length;
if (!G.passOrder.includes(ctx.playOrder[playOrderPos] + '')) {
return playOrderPos;
}
}
}
}
};
/*
* 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.
*/
var MAKE_MOVE = 'MAKE_MOVE';
var GAME_EVENT = 'GAME_EVENT';
var REDO = 'REDO';
var RESET = 'RESET';
var SYNC = 'SYNC';
var UNDO = 'UNDO';
var UPDATE = 'UPDATE';
/*
* 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 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.
*/
var automaticGameEvent = function automaticGameEvent(type, args, playerID, credentials) {
return {
type: GAME_EVENT,
payload: {
type: type,
args: args,
playerID: playerID,
credentials: credentials
},
automatic: true
};
};
// 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(ctx) {
_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 = ctx._random || {
seed: '0'
};
}
/**
* Updates ctx with the PRNG state.
* @param {object} ctx - The ctx object to update.
*/
_createClass(Random, [{
key: "update",
value: function update(state) {
var ctx = _objectSpread({}, state.ctx, {
_random: this.state
});
return _objectSpread({}, state, {
ctx: ctx
});
}
/**
* Attaches the Random API to ctx.
* @param {object} ctx - The ctx object to attach to.
*/
}, {
key: "attach",
value: function attach(ctx) {
return _objectSpread({}, ctx, {
random: this._api()
});
}
/**
* Generate a random number.
*/
}, {
key: "_random",
value: function _random() {
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 = _objectSpread({}, 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 _objectSpread({}, 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;
}
});
}
}]);
return Random;
}();
/**
* Removes the attached Random api from ctx.
*
* @param {object} ctx - The ctx object with the Random API attached.
* @returns {object} A plain ctx object without the Random API.
*/
Random.detach = function (ctx) {
var random = ctx.random,
rest = _objectWithoutProperties(ctx, ["random"]); // eslint-disable-line no-unused-vars
return rest;
};
/**
* Generates a new seed from the current date / time.
*/
Random.seed = function () {
return (+new Date()).toString(36).slice(-10);
};
/**
* 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: "attach",
value: function attach(ctx) {
var _this = this;
var events = {};
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
});
};
};
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;
}
}
}
return _objectSpread({}, ctx, {
events: events
});
}
/**
* Updates ctx with the triggered events.
* @param {object} state - The state object { G, ctx }.
*/
}, {
key: "update",
value: function update$$1(state) {
var _iteratorNormalCompletion2 = true;
var _didIteratorError2 = false;
var _iteratorError2 = undefined;
try {
for (var _iterator2 = this.dispatch[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) {
var item = _step2.value;
var action = automaticGameEvent(item.key, item.args, this.playerID);
state = _objectSpread({}, state, this.flow.processGameEvent(state, action));
}
} catch (err) {
_didIteratorError2 = true;
_iteratorError2 = err;
} finally {
try {
if (!_iteratorNormalCompletion2 && _iterator2.return != null) {
_iterator2.return();
}
} finally {
if (_didIteratorError2) {
throw _iteratorError2;
}
}
}
return state;
}
}]);
return Events;
}();
/**
* Detaches the Events API from ctx.
* @param {object} ctx - The ctx object to strip.
*/
Events.detach = function (ctx) {
var events = ctx.events,
rest = _objectWithoutProperties(ctx, ["events"]); // eslint-disable-line no-unused-vars
return rest;
};
/**
* Moves can return this when they want to indicate
* that the combination of arguments is illegal and
* the move ought to be discarded.
*/
var INVALID_MOVE = 'INVALID_MOVE';
/**
* Context API to allow writing custom logs in games.
*/
var GameLoggerCtxAPI =
/*#__PURE__*/
function () {
function GameLoggerCtxAPI() {
_classCallCheck(this, GameLoggerCtxAPI);
this._payload = undefined;
}
_createClass(GameLoggerCtxAPI, [{
key: "_api",
value: function _api() {
var _this = this;
return {
setPayload: function setPayload(payload) {
_this._payload = payload;
}
};
}
}, {
key: "attach",
value: function attach(ctx$$1) {
return _objectSpread({}, ctx$$1, {
log: this._api()
});
}
}, {
key: "update",
value: function update(state) {
if (this._payload === undefined) {
return state;
} // attach the payload to the last log event
var deltalog = state.deltalog;
deltalog[deltalog.length - 1] = _objectSpread({}, deltalog[deltalog.length - 1], {
payload: this._payload
});
this._payload = undefined;
return _objectSpread({}, state, {
deltalog: deltalog
});
}
}], [{
key: "detach",
value: function detach(ctx$$1) {
var log = ctx$$1.log,
ctxWithoutLog = _objectWithoutProperties(ctx$$1, ["log"]); // eslint-disable-line no-unused-vars
return ctxWithoutLog;
}
}]);
return GameLoggerCtxAPI;
}();
/**
* This class is used to attach/detach various utility objects
* onto a ctx, without having to manually attach/detach them
* all separately.
*/
var ContextEnhancer =
/*#__PURE__*/
function () {
function ContextEnhancer(ctx$$1, game, player) {
_classCallCheck(this, ContextEnhancer);
this.random = new Random(ctx$$1);
this.events = new Events(game.flow, player);
this.log = new GameLoggerCtxAPI();
}
_createClass(ContextEnhancer, [{
key: "attachToContext",
value: function attachToContext(ctx$$1) {
var ctxWithAPI = this.random.attach(ctx$$1);
ctxWithAPI = this.events.attach(ctxWithAPI);
ctxWithAPI = this.log.attach(ctxWithAPI);
return ctxWithAPI;
}
}, {
key: "_update",
value: function _update(state, updateEvents) {
var newState = updateEvents ? this.events.update(state) : state;
newState = this.random.update(newState);
newState = this.log.update(newState);
return newState;
}
}, {
key: "updateAndDetach",
value: function updateAndDetach(state, updateEvents) {
var newState = this._update(state, updateEvents);
newState.ctx = ContextEnhancer.detachAllFromContext(newState.ctx);
return newState;
}
}], [{
key: "detachAllFromContext",
value: function detachAllFromContext(ctx$$1) {
var ctxWithoutAPI = Random.detach(ctx$$1);
ctxWithoutAPI = Events.detach(ctxWithoutAPI);
ctxWithoutAPI = GameLoggerCtxAPI.detach(ctxWithoutAPI);
return ctxWithoutAPI;
}
}]);
return ContextEnhancer;
}();
/**
* InitializeGame
*
* Creates the initial game state.
*
* @param {...object} game - Return value of Game().
* @param {...object} numPlayers - The number of players.
* @param {...object} multiplayer - Set to true if we are in a multiplayer client.
*/
function InitializeGame(_ref) {
var game = _ref.game,
numPlayers = _ref.numPlayers,
setupData = _ref.setupData;
if (!numPlayers) {
numPlayers = 2;
}
var ctx$$1 = game.flow.ctx(numPlayers);
var seed = game.seed;
if (seed === undefined) {
seed = Random.seed();
}
ctx$$1._random = {
seed: seed
}; // Pass ctx through all the plugins that want to modify it.
ctx$$1 = ctx.setup(ctx$$1, game); // Augment ctx with the enhancers (TODO: move these into plugins).
var apiCtx = new ContextEnhancer(ctx$$1, game, ctx$$1.currentPlayer);
var ctxWithAPI = apiCtx.attachToContext(ctx$$1);
var initialG = game.setup(ctxWithAPI, setupData); // Pass G through all the plugins that want to modify it.
initialG = G.setup(initialG, ctxWithAPI, game);
var initial = {
// User managed state.
G: initialG,
// Framework managed state.
ctx: ctx$$1,
// List of {G, ctx} pairs that can be undone.
_undo: [],
// List of {G, ctx} pairs that can be redone.
_redo: [],
// A monotonically non-decreasing ID to ensure that
// state updates are only allowed from clients that
// are at the same version that the server.
_stateID: 0,
// A snapshot of this object so that actions can be
// replayed over it to view old snapshots.
// TODO: This will no longer be necessary once the
// log stops replaying actions (but reads the actual
// game states instead).
_initial: {}
};
var state = game.flow.init({
G: initial.G,
ctx: ctxWithAPI
});
initial.G = state.G;
initial._undo = state._undo;
state = apiCtx.updateAndDetach(state, true);
initial.ctx = state.ctx;
var deepCopy = function deepCopy(obj) {
return flatted.parse(flatted.stringify(obj));
};
initial._initial = deepCopy(initial);
return initial;
}
/**
* CreateGameReducer
*
* Creates the main game state reducer.
* @param {...object} game - Return value of Game().
* @param {...object} numPlayers - The number of players.
* @param {...object} multiplayer - Set to true if we are in a multiplayer client.
*/
function CreateGameReducer(_ref2) {
var game = _ref2.game,
multiplayer = _ref2.multiplayer;
/**
* GameReducer
*
* Redux reducer that maintains the overall game state.
* @param {object} state - The state before the action.
* @param {object} action - A Redux action.
*/
return function () {
var state = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : null;
var action = arguments.length > 1 ? arguments[1] : undefined;
switch (action.type) {
case GAME_EVENT:
{
state = _objectSpread({}, state, {
deltalog: []
}); // Process game events only on the server.
// These events like `endTurn` typically
// contain code that may rely on secret state
// and cannot be computed on the client.
if (multiplayer) {
return state;
} // Ignore the event if the player isn't allowed to make it.
if (action.payload.playerID !== null && action.payload.playerID !== undefined && !game.flow.canPlayerCallEvent(state.G, state.ctx, action.payload.playerID)) {
return state;
}
var apiCtx = new ContextEnhancer(state.ctx, game, action.payload.playerID);
state.ctx = apiCtx.attachToContext(state.ctx);
var newState = game.flow.processGameEvent(state, action);
newState = apiCtx.updateAndDetach(newState, true);
return _objectSpread({}, newState, {
_stateID: state._stateID + 1
});
}
case MAKE_MOVE:
{
state = _objectSpread({}, state, {
deltalog: []
}); // Check whether the game knows the move at all.
if (!game.moveNames.includes(action.payload.type)) {
return state;
} // Ignore the move if it isn't allowed at this point.
if (!game.flow.canMakeMove(state.G, state.ctx, action.payload.type)) {
return state;
} // Ignore the move if the player isn't allowed to make it.
if (action.payload.playerID !== null && action.payload.playerID !== undefined && !game.flow.canPlayerMakeMove(state.G, state.ctx, action.payload.playerID)) {
return state;
}
var _apiCtx = new ContextEnhancer(state.ctx, game, action.payload.playerID);
var ctxWithAPI = _apiCtx.attachToContext(state.ctx); // Process the move.
var G$$1 = game.processMove(state.G, action.payload, ctxWithAPI);
if (G$$1 === INVALID_MOVE) {
// the game declared the move as invalid.
return state;
} // Create a log entry for this move.
var logEntry = {
action: action,
_stateID: state._stateID,
turn: state.ctx.turn,
phase: state.ctx.phase
}; // don't call into events here
var _newState = _apiCtx.updateAndDetach(_objectSpread({}, state, {
deltalog: [logEntry]
}), false);
var ctx$$1 = _newState.ctx; // Undo changes to G if the move should not run on the client.
if (multiplayer && !game.flow.optimisticUpdate(G$$1, ctx$$1, action.payload)) {
G$$1 = state.G;
}
state = _objectSpread({}, _newState, {
G: G$$1,
ctx: ctx$$1,
_stateID: state._stateID + 1
}); // If we're on the client, just process the move
// and no triggers in multiplayer mode.
// These will be processed on the server, which
// will send back a state update.
if (multiplayer) {
return state;
} // Allow the flow reducer to process any triggers that happen after moves.
ctxWithAPI = _apiCtx.attachToContext(state.ctx);
state = game.flow.processMove(_objectSpread({}, state, {
ctx: ctxWithAPI
}), action.payload);
state = _apiCtx.updateAndDetach(state, true);
state._undo[state._undo.length - 1].ctx = state.ctx;
return state;
}
case RESET:
case UPDATE:
case SYNC:
{
return action.state;
}
case UNDO:
{
var _state = state,
_undo = _state._undo,
_redo = _state._redo;
if (_undo.length < 2) {
return state;
}
var last = _undo[_undo.length - 1];
var restore = _undo[_undo.length - 2]; // Only allow undoable moves to be undone.
if (!game.flow.canUndoMove(state.G, state.ctx, last.moveType)) {
return state;
}
return _objectSpread({}, state, {
G: restore.G,
ctx: restore.ctx,
_undo: _undo.slice(0, _undo.length - 1),
_redo: [last].concat(_toConsumableArray(_redo))
});
}
case REDO:
{
var _state2 = state,
_undo2 = _state2._undo,
_redo2 = _state2._redo;
if (_redo2.length == 0) {
return state;
}
var first = _redo2[0];
return _objectSpread({}, state, {
G: first.G,
ctx: first.ctx,
_undo: [].concat(_toConsumableArray(_undo2), [first]),
_redo: _redo2.slice(1)
});
}
default:
{
return state;
}
}
};
}
/**
* Helper to create a reducer that manages ctx (with the
* ability to also update G).
*
* You probably want to use FlowWithPhases below, but you might
* need to use this directly if you are creating a very customized
* game flow that it cannot handle.
*
* @param {...object} ctx - Function with the signature
* numPlayers => ctx
* that determines the initial value of ctx.
* @param {...object} events - Object containing functions
* named after events that this
* reducer will handle. Each function
* has the following signature:
* ({G, ctx}) => {G, ctx}
* @param {...object} enabledEvents - Map of eventName -> bool indicating
* which events are callable from the client
* or from within moves.
* @param {...object} processMove - A function that's called whenever a move is made.
* (state, action, dispatch) => state.
* @param {...object} optimisticUpdate - (G, ctx, move) => boolean
* Control whether a move should
* be executed optimistically on
* the client while waiting for
* the result of execution from
* the server.
* @param {...object} canMakeMove - (G, ctx, moveName) => boolean
* Predicate to determine whether a
* particular move is allowed at
* this time.
*
* @param {...object} canUndoMove - (G, ctx, moveName) => boolean
* Predicate to determine whether a
* particular move is undoable at this
* time.
*
* @param {Array} redactedMoves - List of moves to be redacted
* from the log.
*/
function Flow(_ref) {
var ctx$$1 = _ref.ctx,
events = _ref.events,
enabledEvents = _ref.enabledEvents,
init = _ref.init,
_processMove = _ref.processMove,
optimisticUpdate = _ref.optimisticUpdate,
_canMakeMove = _ref.canMakeMove,
canUndoMove = _ref.canUndoMove,
redactedMoves = _ref.redactedMoves;
if (!ctx$$1) ctx$$1 = function ctx$$1() {
return {};
};
if (!events) events = {};
if (!enabledEvents) enabledEvents = {};
if (!init) init = function init(state) {
return state;
};
if (!_processMove) _processMove = function processMove(state) {
return state;
};
if (!_canMakeMove) _canMakeMove = function canMakeMove() {
return true;
};
if (!canUndoMove) canUndoMove = function canUndoMove() {
return true;
};
if (optimisticUpdate === undefined) {
optimisticUpdate = function optimisticUpdate() {
return true;
};
}
var dispatch = function dispatch(state, action) {
var payload = action.payload;
if (events.hasOwnProperty(payload.type)) {
var context = {
playerID: payload.playerID,
dispatch: dispatch
};
var logEntry = {
action: action,
_stateID: state._stateID,
turn: state.ctx.turn,
phase: state.ctx.phase
};
var deltalog = [].concat(_toConsumableArray(state.deltalog || []), [logEntry]);
state = _objectSpread({}, state, {
deltalog: deltalog
});
var args = [state].concat(payload.args);
return events[payload.type].apply(context, args);
}
return state;
};
return {
ctx: ctx$$1,
init: init,
canUndoMove: canUndoMove,
redactedMoves: redactedMoves,
eventNames: Object.getOwnPropertyNames(events),
enabledEventNames: Object.getOwnPropertyNames(enabledEvents),
processMove: function processMove(state, action) {
return _processMove(state, action, dispatch);
},
processGameEvent: function processGameEvent(state, action) {
return dispatch(state, action, dispatch);
},
optimisticUpdate: optimisticUpdate,
canPlayerCallEvent: function canPlayerCallEvent(G$$1, ctx$$1, playerID) {
return ctx$$1.currentPlayer == playerID && ctx$$1.actionPlayers.includes(playerID);
},
canPlayerMakeMove: function canPlayerMakeMove(G$$1, ctx$$1, playerID) {
var actionPlayers = ctx$$1.actionPlayers || [];
return actionPlayers.includes(playerID);
},
canMakeMove: function canMakeMove(G$$1, ctx$$1, moveName) {
// Disallow moves once the game is over.
if (ctx$$1.gameover !== undefined) return false; // User-provided move validation.
return _canMakeMove(G$$1, ctx$$1, moveName);
}
};
}
/**
* FlowWithPhases
*
* A very customizable game flow that introduces phases to the
* game. Each phase can be configured with:
* - A custom turn order.
* - Automatically executed setup / cleanup code.
* - Custom phase end conditions.
* - A move whitelist that disallows other moves during the phase.
*
* @param {...object} movesPerTurn - End the turn automatically after a certain number
* of moves (default: undefined, i.e. the turn does
* not automatically end after a certain number of moves).
*
* @param {...object} endTurnIf - The turn automatically ends if this
* returns a truthy value
* (checked after each move).
* If the return value is { next: playerID },
* that player is the next player
* (instead of following the turn order).
* (G, ctx) => boolean|object
*
* @param {...object} endGameIf - The game automatically ends if this function
* returns anything (checked after each move).
* The return value is available at ctx.gameover.
* (G, ctx) => {}
*
* @param {...object} onTurnBegin - Any code to run when a turn begins.
*