dungeoneer
Version:
A procedural dungeon generator
1,530 lines (1,340 loc) • 104 kB
JavaScript
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.Dungeoneer = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(require,module,exports){
/**
* Based on Bob Nystrom's procedural dungeon generation logic that he wrote for Hauberk
* http://journal.stuffwithstuff.com/2014/12/21/rooms-and-mazes/
*/
'use strict';
const Victor = require('victor');
const _ = require('underscore');
const Room = require('./room');
const Tile = require('./tile');
/**
* @desc The random dungeon generator.
*
* Starting with a stage of solid walls, it works like so:
*
* 1. Place a number of randomly sized and positioned rooms. If a room
* overlaps an existing room, it is discarded. Any remaining rooms are
* carved out.
* 2. Any remaining solid areas are filled in with mazes. The maze generator
* will grow and fill in even odd-shaped areas, but will not touch any
* rooms.
* 3. The result of the previous two steps is a series of unconnected rooms
* and mazes. We walk the stage and find every tile that can be a
* "connector". This is a solid tile that is adjacent to two unconnected
* regions.
* 4. We randomly choose connectors and open them or place a door there until
* all of the unconnected regions have been joined. There is also a slight
* chance to carve a connector between two already-joined regions, so that
* the dungeon isn't single connected.
* 5. The mazes will have a lot of dead ends. Finally, we remove those by
* repeatedly filling in any open tile that's closed on three sides. When
* this is done, every corridor in a maze actually leads somewhere.
*
* The end result of this is a multiply-connected dungeon with rooms and lots
* of winding corridors.
*
* @constructor
*/
const Dungeon = function Dungeon() {
var numRoomTries = 50;
// The inverse chance of adding a connector between two regions that have
// already been joined. Increasing this leads to more loosely connected
// dungeons.
var extraConnectorChance = 50;
// Increasing this allows rooms to be larger.
var roomExtraSize = 0;
var windingPercent = 50;
var _rooms = [];
// The index of the current region being carved.
var _currentRegion = -1;
var stage;
const n = new Victor(0, 1);
const e = new Victor(1, 0);
const s = new Victor(0, -1);
const w = new Victor(-1, 0);
// The four cardinal directions: north, south, east, and west.
const cardinalDirections = [n, e, s, w];
const bindStage = givenStage => {
stage = givenStage;
};
let _tiles = [];
/**
* @desc returns a tile at the provided coordinates
*
* @param {Number} x - The x coordinate to retrieve
* @param {Number} y - The y coordinate to retrieve
*
* @returns {Object} - A Tile object
*/
const getTile = (x, y) => {
return _tiles[x][y];
};
/**
* @desc Sets a tile's type and region. This function will thrown an error if
* the tile doesn't exist.
*
* @param {Number} x - The x coordinate of the tile to set
* @param {Number} y - The y coordinate of the tile to set
* @param {String} type - The type to set on the tile
*
* @returns {Object} - The Tile object or null if the tile was not found
*
*/
const setTile = (x, y, type) => {
if (_tiles[x] && _tiles[x][y]) {
_tiles[x][y].type = type;
_tiles[x][y].region = _currentRegion;
return _tiles[x][y];
}
throw new RangeError(`tile at ${x}, ${y} is unreachable`);
};
/**
* @desc Generates tile data to the dimension of the stage.
*
* @param {String} type - The tile type to set on newly created tiles
*
* @returns {Array} - The _tiles array
*/
const fill = type => {
let neighbours = [];
let nesw = {};
var x;
var y;
for (x = 0; x < stage.width; x++) {
_tiles.push([]);
for (y = 0; y < stage.height; y++) {
_tiles[x].push(new Tile(type));
}
}
for (x = 0; x < stage.width; x++) {
for (y = 0; y < stage.height; y++) {
neighbours = [];
nesw = {};
if (_tiles[x][y - 1]) {
neighbours.push(_tiles[x][y - 1]);
nesw.north = _tiles[x][y - 1];
}
if (_tiles[x + 1] && _tiles[x + 1][y - 1]) {
neighbours.push(_tiles[x + 1][y - 1]);
}
if (_tiles[x + 1] && _tiles[x + 1][y]) {
neighbours.push(_tiles[x + 1][y]);
nesw.east = _tiles[x + 1][y];
}
if (_tiles[x + 1] && _tiles[x + 1][y + 1]) {
neighbours.push(_tiles[x + 1][y + 1]);
}
if (_tiles[x] && _tiles[x][y + 1]) {
neighbours.push(_tiles[x][y + 1]);
nesw.south = _tiles[x][y + 1];
}
if (_tiles[x - 1] && _tiles[x - 1][y + 1]) {
neighbours.push(_tiles[x - 1][y + 1]);
}
if (_tiles[x - 1] && _tiles[x - 1][y]) {
neighbours.push(_tiles[x - 1][y]);
nesw.west = _tiles[x - 1][y];
}
if (_tiles[x - 1] && _tiles[x - 1][y - 1]) {
neighbours.push(_tiles[x - 1][y - 1]);
}
_tiles[x][y].setNeighbours(neighbours);
_tiles[x][y].nesw = nesw;
}
}
return _tiles;
};
/**
* @desc Master function for generating a dungeon
*
* @param {Object} stage - An object with a width key and a height key. Used
* to determine the size of the dungeon. Must be odd with and height.
* @param {Boolean} debug - outputs debug info if set to true
*
* @returns {Object} - Tile information for the dungeon
*/
const generate = (stage, debug = false) => {
let startDate = Date.now();
if (stage.width % 2 === 0 || stage.height % 2 === 0) {
throw new Error('The stage must be odd-sized.');
}
bindStage(stage);
fill('wall');
_addRooms();
// Fill in all of the empty space with mazes.
for (var y = 1; y < stage.height; y += 2) {
for (var x = 1; x < stage.width; x += 2) {
// Skip the maze generation if the tile is already carved
if (getTile(x, y).type === 'floor') {
continue;
}
_growMaze(x, y);
}
}
_connectRegions();
_removeDeadEnds();
let endDate = Date.now();
if (debug) {
console.log('Dungeon generated in ' + (endDate - startDate) + 'ms');
}
return {
rooms: _rooms,
tiles: _tiles
};
};
/**
* @desc Implementation of the "growing tree" algorithm from here:
* http://www.astrolog.org/labyrnth/algrithm.htm.
*
* @param {Number} startX - The x coordinate to start at
* @param {Number} startY - The y coordinate to start at
*
* @returns {void}
*/
const _growMaze = (startX, startY) => {
var cells = [];
var lastDir;
if (_tiles[startX][startY].neighbours.filter(x => x.type === 'floor').length > 0) {
return;
}
_startRegion();
_carve(startX, startY);
cells.push(new Victor(startX, startY));
let count = 0;
while (cells.length && count < 500) {
count++;
var cell = cells[cells.length - 1];
// See which adjacent cells are open.
var unmadeCells = [];
for (let dir of cardinalDirections) {
if (_canCarve(cell, dir)) {
unmadeCells.push(dir);
}
}
if (unmadeCells.length) {
// Based on how "windy" passages are, try to prefer carving in the
// same direction.
var dir;
var stringifiedCells = unmadeCells.map(v => v.toString());
if (lastDir && stringifiedCells.indexOf(lastDir.toString()) > -1 && _.random(1, 100) > windingPercent) {
dir = lastDir.clone();
} else {
let rand = _.random(0, unmadeCells.length - 1);
dir = unmadeCells[rand].clone();
}
let carveLoc1 = cell.clone().add(dir).toObject();
_carve(carveLoc1.x, carveLoc1.y);
let carveLoc2 = cell.clone().add(dir).add(dir).toObject();
_carve(carveLoc2.x, carveLoc2.y);
cells.push(cell.clone().add(dir).add(dir));
lastDir = dir.clone();
} else {
// No adjacent uncarved cells.
cells.pop();
// This path has ended.
lastDir = null;
}
}
};
/**
* @desc Creates rooms in the dungeon by repeatedly creating random rooms and
* seeing if they overlap. Rooms that overlap are discarded. This process is
* repeated until it hits the maximum tries determined by the 'numRoomTries'
* variable.
*
* @returns {void}
*/
const _addRooms = () => {
for (var i = 0; i < numRoomTries; i++) {
// Pick a random room size. The funny math here does two things:
// - It makes sure rooms are odd-sized to line up with maze.
// - It avoids creating rooms that are too rectangular: too tall and
// narrow or too wide and flat.
var size = _.random(1, 3 + roomExtraSize) * 2 + 1;
var rectangularity = _.random(0, 1 + Math.floor(size / 2)) * 2;
var width = size;
var height = size;
if (_oneIn(2)) {
width += rectangularity;
} else {
height += rectangularity;
}
var x = _.random(0, Math.floor((stage.width - width) / 2)) * 2 + 1;
var y = _.random(0, Math.floor((stage.height - height) / 2)) * 2 + 1;
if (x > stage.width - width) {
x = stage.width - width - 1;
}
if (y > stage.height - height) {
y = stage.height - height - 1;
}
var room = new Room(x, y, width, height);
var overlaps = false;
for (var other of _rooms) {
if (room.intersects(other)) {
overlaps = true;
break;
}
}
if (overlaps) {
continue;
}
_rooms.push(room);
_startRegion();
// Convert room tiles to floor
carveArea(x, y, width, height);
}
};
/**
* @desc converts an area of tiles to floor type
*
* @param {Number} x - The starting x coordinate
* @param {Number} y - The starting y coordinate
* @param {Number} width - The width of the area to carve
* @param {Number} height - The height of the area to carve
*
* @returns {void}
*/
const carveArea = (x, y, width, height) => {
for (var i = x; i < x + width; i++) {
for (var j = y; j < y + height; j++) {
_carve(i, j);
}
}
};
/**
* @desc Creates doorways between each generated region of tiles
*
* @return {void}
*/
const _connectRegions = () => {
let regionConnections = {};
_tiles.forEach(row => {
row.forEach(tile => {
if (tile.type === 'floor') {
return;
}
let tileRegions = _.unique(_.values(tile.nesw).map(x => x.region).filter(x => !_.isUndefined(x)));
if (tileRegions.length <= 1) {
return;
}
let key = tileRegions.join('-');
if (!regionConnections[key]) {
regionConnections[key] = [];
}
regionConnections[key].push(tile);
});
});
_.each(regionConnections, connections => {
let index = _.random(0, connections.length - 1);
connections[index].type = 'door';
connections.splice(index, 1);
// Occasional open up additional connections
connections.forEach(conn => {
if (_oneIn(extraConnectorChance)) {
conn.type = 'door';
}
});
});
};
/**
* @desc Helper function for calculating random chance. The higher the number
* provided the less likely this value is to return true.
*
* @param {Number} num - The ceiling number that could be calculated
*
* @returns {Boolean} - True if the function rolled a one
*
* @example
* _oneIn(50); // - Has a 1 in 50 chance of returning true
*/
const _oneIn = num => {
return _.random(1, num) === 1;
};
/**
* @desc Fills in dead ends in the dungeon with wall tiles
*
* @returns {void}
*/
const _removeDeadEnds = () => {
var done = false;
const cycle = () => {
let done = true;
_tiles.forEach(row => {
row.forEach(tile => {
// If it only has one exit, it's a dead end --> fill it in!
if (tile.type === 'wall') {
return;
}
if (_.values(tile.nesw).filter(t => t.type !== 'wall').length <= 1) {
tile.type = 'wall';
done = false;
}
});
});
return done;
};
while (!done) {
done = true;
done = cycle();
}
};
/**
* @desc Gets whether or not an opening can be carved from the given starting
* [Cell] at [pos] to the adjacent Cell facing [direction]. Returns `true`
* if the starting Cell is in bounds and the destination Cell is filled
* (or out of bounds).</returns>
*
* @param {Victor} cell - Victor JS vector object
* @param {Victor} direction - Victor JS vector object indicating direction
*
* @return {Boolean} - true if the path can be carved
*/
const _canCarve = (cell, direction) => {
// Must end in bounds.
let end = cell.clone().add(direction).add(direction).add(direction).toObject();
if (!_tiles[end.x] || !_tiles[end.x][end.y]) {
return false;
}
if (getTile(end.x, end.y).type !== 'wall') {
return false;
}
// Destination must not be open.
let dest = cell.clone().add(direction).add(direction).toObject();
return getTile(dest.x, dest.y).type !== 'floor';
};
/**
* @desc Increments the current region. Typically called every time a new area
* starts being carved
*
* @returns {Number} - The current region number
*/
const _startRegion = () => {
_currentRegion++;
return _currentRegion;
};
/**
* @desc Changes the Tile at a given coordinate to a provided type. Typically
* used to change the type to 'floor'
*
* @param {Number} x - The x coordinate to change
* @param {Number} y - The y coordinate to change
* @param {String} type - The type to change the tile to. Defaults to 'floor'
*
* @returns {void}
*/
const _carve = (x, y, type = 'floor') => {
setTile(x, y, type);
};
return {
generate
};
};
const generate = options => {
return new Dungeon().generate(options);
};
module.exports = {
generate
};
},{"./room":2,"./tile":3,"underscore":4,"victor":5}],2:[function(require,module,exports){
'use strict';
/**
* @desc Helper class for drawing rooms when generating dungeons
* @constructor
*
* @param {Number} x - The x coordinate of the top side of the room
* @param {Number} y - The y coordinate of the left hand side of the room
* @param {Number} width - The width of the room
* @param {Number} height - The height of the room
*/
const Room = function Room(x, y, width, height) {
this.x = x;
this.y = y;
this.width = width;
this.height = height;
};
/**
* @desc Returns the bounding box for this room
* @function
*
* @returns {Object} - Bounding box object containing a top, right, bottom and
* left value.
*/
Room.prototype.getBoundingBox = function getBoundingBox() {
return {
top: this.y,
right: this.x + this.width,
bottom: this.y + this.height,
left: this.x
};
};
/**
* @desc Compares this room with an entity that has a bounding box method to see
* if they intersect.
*
* @param {Object} other - An object with a getBoundingBox() method
*
* @returns {Boolean} - true if there is an intersection
*/
Room.prototype.intersects = function intersects(other) {
if (!other.getBoundingBox) {
throw new Error('Given entity has no method getBoundingBox');
}
var r1 = this.getBoundingBox();
var r2 = other.getBoundingBox();
return !(r2.left > r1.right || r2.right < r1.left || r2.top > r1.bottom || r2.bottom < r1.top);
};
module.exports = Room;
},{}],3:[function(require,module,exports){
'use strict';
/**
* @desc Class for a single tilein a dungeon
* @constructor
*
* @param {String} type - The type of tile, e.g. 'wall', 'floor'
*/
const Tile = function Tile(type) {
this.type = type;
this.neighbours = [];
};
/**
* @desc Sets an array containing this tiles immediate neighbours
*
* @param {Object[]} neighbours - An array of neighbouring Tiles
*
* @return {Object} - returns the Tile object, useful for chaining
*/
Tile.prototype.setNeighbours = function (neighbours) {
this.neighbours = neighbours;
return this;
};
module.exports = Tile;
},{}],4:[function(require,module,exports){
(function (global){
// Underscore.js 1.9.1
// http://underscorejs.org
// (c) 2009-2018 Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors
// Underscore may be freely distributed under the MIT license.
(function() {
// Baseline setup
// --------------
// Establish the root object, `window` (`self`) in the browser, `global`
// on the server, or `this` in some virtual machines. We use `self`
// instead of `window` for `WebWorker` support.
var root = typeof self == 'object' && self.self === self && self ||
typeof global == 'object' && global.global === global && global ||
this ||
{};
// Save the previous value of the `_` variable.
var previousUnderscore = root._;
// Save bytes in the minified (but not gzipped) version:
var ArrayProto = Array.prototype, ObjProto = Object.prototype;
var SymbolProto = typeof Symbol !== 'undefined' ? Symbol.prototype : null;
// Create quick reference variables for speed access to core prototypes.
var push = ArrayProto.push,
slice = ArrayProto.slice,
toString = ObjProto.toString,
hasOwnProperty = ObjProto.hasOwnProperty;
// All **ECMAScript 5** native function implementations that we hope to use
// are declared here.
var nativeIsArray = Array.isArray,
nativeKeys = Object.keys,
nativeCreate = Object.create;
// Naked function reference for surrogate-prototype-swapping.
var Ctor = function(){};
// Create a safe reference to the Underscore object for use below.
var _ = function(obj) {
if (obj instanceof _) return obj;
if (!(this instanceof _)) return new _(obj);
this._wrapped = obj;
};
// Export the Underscore object for **Node.js**, with
// backwards-compatibility for their old module API. If we're in
// the browser, add `_` as a global object.
// (`nodeType` is checked to ensure that `module`
// and `exports` are not HTML elements.)
if (typeof exports != 'undefined' && !exports.nodeType) {
if (typeof module != 'undefined' && !module.nodeType && module.exports) {
exports = module.exports = _;
}
exports._ = _;
} else {
root._ = _;
}
// Current version.
_.VERSION = '1.9.1';
// Internal function that returns an efficient (for current engines) version
// of the passed-in callback, to be repeatedly applied in other Underscore
// functions.
var optimizeCb = function(func, context, argCount) {
if (context === void 0) return func;
switch (argCount == null ? 3 : argCount) {
case 1: return function(value) {
return func.call(context, value);
};
// The 2-argument case is omitted because we’re not using it.
case 3: return function(value, index, collection) {
return func.call(context, value, index, collection);
};
case 4: return function(accumulator, value, index, collection) {
return func.call(context, accumulator, value, index, collection);
};
}
return function() {
return func.apply(context, arguments);
};
};
var builtinIteratee;
// An internal function to generate callbacks that can be applied to each
// element in a collection, returning the desired result — either `identity`,
// an arbitrary callback, a property matcher, or a property accessor.
var cb = function(value, context, argCount) {
if (_.iteratee !== builtinIteratee) return _.iteratee(value, context);
if (value == null) return _.identity;
if (_.isFunction(value)) return optimizeCb(value, context, argCount);
if (_.isObject(value) && !_.isArray(value)) return _.matcher(value);
return _.property(value);
};
// External wrapper for our callback generator. Users may customize
// `_.iteratee` if they want additional predicate/iteratee shorthand styles.
// This abstraction hides the internal-only argCount argument.
_.iteratee = builtinIteratee = function(value, context) {
return cb(value, context, Infinity);
};
// Some functions take a variable number of arguments, or a few expected
// arguments at the beginning and then a variable number of values to operate
// on. This helper accumulates all remaining arguments past the function’s
// argument length (or an explicit `startIndex`), into an array that becomes
// the last argument. Similar to ES6’s "rest parameter".
var restArguments = function(func, startIndex) {
startIndex = startIndex == null ? func.length - 1 : +startIndex;
return function() {
var length = Math.max(arguments.length - startIndex, 0),
rest = Array(length),
index = 0;
for (; index < length; index++) {
rest[index] = arguments[index + startIndex];
}
switch (startIndex) {
case 0: return func.call(this, rest);
case 1: return func.call(this, arguments[0], rest);
case 2: return func.call(this, arguments[0], arguments[1], rest);
}
var args = Array(startIndex + 1);
for (index = 0; index < startIndex; index++) {
args[index] = arguments[index];
}
args[startIndex] = rest;
return func.apply(this, args);
};
};
// An internal function for creating a new object that inherits from another.
var baseCreate = function(prototype) {
if (!_.isObject(prototype)) return {};
if (nativeCreate) return nativeCreate(prototype);
Ctor.prototype = prototype;
var result = new Ctor;
Ctor.prototype = null;
return result;
};
var shallowProperty = function(key) {
return function(obj) {
return obj == null ? void 0 : obj[key];
};
};
var has = function(obj, path) {
return obj != null && hasOwnProperty.call(obj, path);
}
var deepGet = function(obj, path) {
var length = path.length;
for (var i = 0; i < length; i++) {
if (obj == null) return void 0;
obj = obj[path[i]];
}
return length ? obj : void 0;
};
// Helper for collection methods to determine whether a collection
// should be iterated as an array or as an object.
// Related: http://people.mozilla.org/~jorendorff/es6-draft.html#sec-tolength
// Avoids a very nasty iOS 8 JIT bug on ARM-64. #2094
var MAX_ARRAY_INDEX = Math.pow(2, 53) - 1;
var getLength = shallowProperty('length');
var isArrayLike = function(collection) {
var length = getLength(collection);
return typeof length == 'number' && length >= 0 && length <= MAX_ARRAY_INDEX;
};
// Collection Functions
// --------------------
// The cornerstone, an `each` implementation, aka `forEach`.
// Handles raw objects in addition to array-likes. Treats all
// sparse array-likes as if they were dense.
_.each = _.forEach = function(obj, iteratee, context) {
iteratee = optimizeCb(iteratee, context);
var i, length;
if (isArrayLike(obj)) {
for (i = 0, length = obj.length; i < length; i++) {
iteratee(obj[i], i, obj);
}
} else {
var keys = _.keys(obj);
for (i = 0, length = keys.length; i < length; i++) {
iteratee(obj[keys[i]], keys[i], obj);
}
}
return obj;
};
// Return the results of applying the iteratee to each element.
_.map = _.collect = function(obj, iteratee, context) {
iteratee = cb(iteratee, context);
var keys = !isArrayLike(obj) && _.keys(obj),
length = (keys || obj).length,
results = Array(length);
for (var index = 0; index < length; index++) {
var currentKey = keys ? keys[index] : index;
results[index] = iteratee(obj[currentKey], currentKey, obj);
}
return results;
};
// Create a reducing function iterating left or right.
var createReduce = function(dir) {
// Wrap code that reassigns argument variables in a separate function than
// the one that accesses `arguments.length` to avoid a perf hit. (#1991)
var reducer = function(obj, iteratee, memo, initial) {
var keys = !isArrayLike(obj) && _.keys(obj),
length = (keys || obj).length,
index = dir > 0 ? 0 : length - 1;
if (!initial) {
memo = obj[keys ? keys[index] : index];
index += dir;
}
for (; index >= 0 && index < length; index += dir) {
var currentKey = keys ? keys[index] : index;
memo = iteratee(memo, obj[currentKey], currentKey, obj);
}
return memo;
};
return function(obj, iteratee, memo, context) {
var initial = arguments.length >= 3;
return reducer(obj, optimizeCb(iteratee, context, 4), memo, initial);
};
};
// **Reduce** builds up a single result from a list of values, aka `inject`,
// or `foldl`.
_.reduce = _.foldl = _.inject = createReduce(1);
// The right-associative version of reduce, also known as `foldr`.
_.reduceRight = _.foldr = createReduce(-1);
// Return the first value which passes a truth test. Aliased as `detect`.
_.find = _.detect = function(obj, predicate, context) {
var keyFinder = isArrayLike(obj) ? _.findIndex : _.findKey;
var key = keyFinder(obj, predicate, context);
if (key !== void 0 && key !== -1) return obj[key];
};
// Return all the elements that pass a truth test.
// Aliased as `select`.
_.filter = _.select = function(obj, predicate, context) {
var results = [];
predicate = cb(predicate, context);
_.each(obj, function(value, index, list) {
if (predicate(value, index, list)) results.push(value);
});
return results;
};
// Return all the elements for which a truth test fails.
_.reject = function(obj, predicate, context) {
return _.filter(obj, _.negate(cb(predicate)), context);
};
// Determine whether all of the elements match a truth test.
// Aliased as `all`.
_.every = _.all = function(obj, predicate, context) {
predicate = cb(predicate, context);
var keys = !isArrayLike(obj) && _.keys(obj),
length = (keys || obj).length;
for (var index = 0; index < length; index++) {
var currentKey = keys ? keys[index] : index;
if (!predicate(obj[currentKey], currentKey, obj)) return false;
}
return true;
};
// Determine if at least one element in the object matches a truth test.
// Aliased as `any`.
_.some = _.any = function(obj, predicate, context) {
predicate = cb(predicate, context);
var keys = !isArrayLike(obj) && _.keys(obj),
length = (keys || obj).length;
for (var index = 0; index < length; index++) {
var currentKey = keys ? keys[index] : index;
if (predicate(obj[currentKey], currentKey, obj)) return true;
}
return false;
};
// Determine if the array or object contains a given item (using `===`).
// Aliased as `includes` and `include`.
_.contains = _.includes = _.include = function(obj, item, fromIndex, guard) {
if (!isArrayLike(obj)) obj = _.values(obj);
if (typeof fromIndex != 'number' || guard) fromIndex = 0;
return _.indexOf(obj, item, fromIndex) >= 0;
};
// Invoke a method (with arguments) on every item in a collection.
_.invoke = restArguments(function(obj, path, args) {
var contextPath, func;
if (_.isFunction(path)) {
func = path;
} else if (_.isArray(path)) {
contextPath = path.slice(0, -1);
path = path[path.length - 1];
}
return _.map(obj, function(context) {
var method = func;
if (!method) {
if (contextPath && contextPath.length) {
context = deepGet(context, contextPath);
}
if (context == null) return void 0;
method = context[path];
}
return method == null ? method : method.apply(context, args);
});
});
// Convenience version of a common use case of `map`: fetching a property.
_.pluck = function(obj, key) {
return _.map(obj, _.property(key));
};
// Convenience version of a common use case of `filter`: selecting only objects
// containing specific `key:value` pairs.
_.where = function(obj, attrs) {
return _.filter(obj, _.matcher(attrs));
};
// Convenience version of a common use case of `find`: getting the first object
// containing specific `key:value` pairs.
_.findWhere = function(obj, attrs) {
return _.find(obj, _.matcher(attrs));
};
// Return the maximum element (or element-based computation).
_.max = function(obj, iteratee, context) {
var result = -Infinity, lastComputed = -Infinity,
value, computed;
if (iteratee == null || typeof iteratee == 'number' && typeof obj[0] != 'object' && obj != null) {
obj = isArrayLike(obj) ? obj : _.values(obj);
for (var i = 0, length = obj.length; i < length; i++) {
value = obj[i];
if (value != null && value > result) {
result = value;
}
}
} else {
iteratee = cb(iteratee, context);
_.each(obj, function(v, index, list) {
computed = iteratee(v, index, list);
if (computed > lastComputed || computed === -Infinity && result === -Infinity) {
result = v;
lastComputed = computed;
}
});
}
return result;
};
// Return the minimum element (or element-based computation).
_.min = function(obj, iteratee, context) {
var result = Infinity, lastComputed = Infinity,
value, computed;
if (iteratee == null || typeof iteratee == 'number' && typeof obj[0] != 'object' && obj != null) {
obj = isArrayLike(obj) ? obj : _.values(obj);
for (var i = 0, length = obj.length; i < length; i++) {
value = obj[i];
if (value != null && value < result) {
result = value;
}
}
} else {
iteratee = cb(iteratee, context);
_.each(obj, function(v, index, list) {
computed = iteratee(v, index, list);
if (computed < lastComputed || computed === Infinity && result === Infinity) {
result = v;
lastComputed = computed;
}
});
}
return result;
};
// Shuffle a collection.
_.shuffle = function(obj) {
return _.sample(obj, Infinity);
};
// Sample **n** random values from a collection using the modern version of the
// [Fisher-Yates shuffle](http://en.wikipedia.org/wiki/Fisher–Yates_shuffle).
// If **n** is not specified, returns a single random element.
// The internal `guard` argument allows it to work with `map`.
_.sample = function(obj, n, guard) {
if (n == null || guard) {
if (!isArrayLike(obj)) obj = _.values(obj);
return obj[_.random(obj.length - 1)];
}
var sample = isArrayLike(obj) ? _.clone(obj) : _.values(obj);
var length = getLength(sample);
n = Math.max(Math.min(n, length), 0);
var last = length - 1;
for (var index = 0; index < n; index++) {
var rand = _.random(index, last);
var temp = sample[index];
sample[index] = sample[rand];
sample[rand] = temp;
}
return sample.slice(0, n);
};
// Sort the object's values by a criterion produced by an iteratee.
_.sortBy = function(obj, iteratee, context) {
var index = 0;
iteratee = cb(iteratee, context);
return _.pluck(_.map(obj, function(value, key, list) {
return {
value: value,
index: index++,
criteria: iteratee(value, key, list)
};
}).sort(function(left, right) {
var a = left.criteria;
var b = right.criteria;
if (a !== b) {
if (a > b || a === void 0) return 1;
if (a < b || b === void 0) return -1;
}
return left.index - right.index;
}), 'value');
};
// An internal function used for aggregate "group by" operations.
var group = function(behavior, partition) {
return function(obj, iteratee, context) {
var result = partition ? [[], []] : {};
iteratee = cb(iteratee, context);
_.each(obj, function(value, index) {
var key = iteratee(value, index, obj);
behavior(result, value, key);
});
return result;
};
};
// Groups the object's values by a criterion. Pass either a string attribute
// to group by, or a function that returns the criterion.
_.groupBy = group(function(result, value, key) {
if (has(result, key)) result[key].push(value); else result[key] = [value];
});
// Indexes the object's values by a criterion, similar to `groupBy`, but for
// when you know that your index values will be unique.
_.indexBy = group(function(result, value, key) {
result[key] = value;
});
// Counts instances of an object that group by a certain criterion. Pass
// either a string attribute to count by, or a function that returns the
// criterion.
_.countBy = group(function(result, value, key) {
if (has(result, key)) result[key]++; else result[key] = 1;
});
var reStrSymbol = /[^\ud800-\udfff]|[\ud800-\udbff][\udc00-\udfff]|[\ud800-\udfff]/g;
// Safely create a real, live array from anything iterable.
_.toArray = function(obj) {
if (!obj) return [];
if (_.isArray(obj)) return slice.call(obj);
if (_.isString(obj)) {
// Keep surrogate pair characters together
return obj.match(reStrSymbol);
}
if (isArrayLike(obj)) return _.map(obj, _.identity);
return _.values(obj);
};
// Return the number of elements in an object.
_.size = function(obj) {
if (obj == null) return 0;
return isArrayLike(obj) ? obj.length : _.keys(obj).length;
};
// Split a collection into two arrays: one whose elements all satisfy the given
// predicate, and one whose elements all do not satisfy the predicate.
_.partition = group(function(result, value, pass) {
result[pass ? 0 : 1].push(value);
}, true);
// Array Functions
// ---------------
// Get the first element of an array. Passing **n** will return the first N
// values in the array. Aliased as `head` and `take`. The **guard** check
// allows it to work with `_.map`.
_.first = _.head = _.take = function(array, n, guard) {
if (array == null || array.length < 1) return n == null ? void 0 : [];
if (n == null || guard) return array[0];
return _.initial(array, array.length - n);
};
// Returns everything but the last entry of the array. Especially useful on
// the arguments object. Passing **n** will return all the values in
// the array, excluding the last N.
_.initial = function(array, n, guard) {
return slice.call(array, 0, Math.max(0, array.length - (n == null || guard ? 1 : n)));
};
// Get the last element of an array. Passing **n** will return the last N
// values in the array.
_.last = function(array, n, guard) {
if (array == null || array.length < 1) return n == null ? void 0 : [];
if (n == null || guard) return array[array.length - 1];
return _.rest(array, Math.max(0, array.length - n));
};
// Returns everything but the first entry of the array. Aliased as `tail` and `drop`.
// Especially useful on the arguments object. Passing an **n** will return
// the rest N values in the array.
_.rest = _.tail = _.drop = function(array, n, guard) {
return slice.call(array, n == null || guard ? 1 : n);
};
// Trim out all falsy values from an array.
_.compact = function(array) {
return _.filter(array, Boolean);
};
// Internal implementation of a recursive `flatten` function.
var flatten = function(input, shallow, strict, output) {
output = output || [];
var idx = output.length;
for (var i = 0, length = getLength(input); i < length; i++) {
var value = input[i];
if (isArrayLike(value) && (_.isArray(value) || _.isArguments(value))) {
// Flatten current level of array or arguments object.
if (shallow) {
var j = 0, len = value.length;
while (j < len) output[idx++] = value[j++];
} else {
flatten(value, shallow, strict, output);
idx = output.length;
}
} else if (!strict) {
output[idx++] = value;
}
}
return output;
};
// Flatten out an array, either recursively (by default), or just one level.
_.flatten = function(array, shallow) {
return flatten(array, shallow, false);
};
// Return a version of the array that does not contain the specified value(s).
_.without = restArguments(function(array, otherArrays) {
return _.difference(array, otherArrays);
});
// Produce a duplicate-free version of the array. If the array has already
// been sorted, you have the option of using a faster algorithm.
// The faster algorithm will not work with an iteratee if the iteratee
// is not a one-to-one function, so providing an iteratee will disable
// the faster algorithm.
// Aliased as `unique`.
_.uniq = _.unique = function(array, isSorted, iteratee, context) {
if (!_.isBoolean(isSorted)) {
context = iteratee;
iteratee = isSorted;
isSorted = false;
}
if (iteratee != null) iteratee = cb(iteratee, context);
var result = [];
var seen = [];
for (var i = 0, length = getLength(array); i < length; i++) {
var value = array[i],
computed = iteratee ? iteratee(value, i, array) : value;
if (isSorted && !iteratee) {
if (!i || seen !== computed) result.push(value);
seen = computed;
} else if (iteratee) {
if (!_.contains(seen, computed)) {
seen.push(computed);
result.push(value);
}
} else if (!_.contains(result, value)) {
result.push(value);
}
}
return result;
};
// Produce an array that contains the union: each distinct element from all of
// the passed-in arrays.
_.union = restArguments(function(arrays) {
return _.uniq(flatten(arrays, true, true));
});
// Produce an array that contains every item shared between all the
// passed-in arrays.
_.intersection = function(array) {
var result = [];
var argsLength = arguments.length;
for (var i = 0, length = getLength(array); i < length; i++) {
var item = array[i];
if (_.contains(result, item)) continue;
var j;
for (j = 1; j < argsLength; j++) {
if (!_.contains(arguments[j], item)) break;
}
if (j === argsLength) result.push(item);
}
return result;
};
// Take the difference between one array and a number of other arrays.
// Only the elements present in just the first array will remain.
_.difference = restArguments(function(array, rest) {
rest = flatten(rest, true, true);
return _.filter(array, function(value){
return !_.contains(rest, value);
});
});
// Complement of _.zip. Unzip accepts an array of arrays and groups
// each array's elements on shared indices.
_.unzip = function(array) {
var length = array && _.max(array, getLength).length || 0;
var result = Array(length);
for (var index = 0; index < length; index++) {
result[index] = _.pluck(array, index);
}
return result;
};
// Zip together multiple lists into a single array -- elements that share
// an index go together.
_.zip = restArguments(_.unzip);
// Converts lists into objects. Pass either a single array of `[key, value]`
// pairs, or two parallel arrays of the same length -- one of keys, and one of
// the corresponding values. Passing by pairs is the reverse of _.pairs.
_.object = function(list, values) {
var result = {};
for (var i = 0, length = getLength(list); i < length; i++) {
if (values) {
result[list[i]] = values[i];
} else {
result[list[i][0]] = list[i][1];
}
}
return result;
};
// Generator function to create the findIndex and findLastIndex functions.
var createPredicateIndexFinder = function(dir) {
return function(array, predicate, context) {
predicate = cb(predicate, context);
var length = getLength(array);
var index = dir > 0 ? 0 : length - 1;
for (; index >= 0 && index < length; index += dir) {
if (predicate(array[index], index, array)) return index;
}
return -1;
};
};
// Returns the first index on an array-like that passes a predicate test.
_.findIndex = createPredicateIndexFinder(1);
_.findLastIndex = createPredicateIndexFinder(-1);
// Use a comparator function to figure out the smallest index at which
// an object should be inserted so as to maintain order. Uses binary search.
_.sortedIndex = function(array, obj, iteratee, context) {
iteratee = cb(iteratee, context, 1);
var value = iteratee(obj);
var low = 0, high = getLength(array);
while (low < high) {
var mid = Math.floor((low + high) / 2);
if (iteratee(array[mid]) < value) low = mid + 1; else high = mid;
}
return low;
};
// Generator function to create the indexOf and lastIndexOf functions.
var createIndexFinder = function(dir, predicateFind, sortedIndex) {
return function(array, item, idx) {
var i = 0, length = getLength(array);
if (typeof idx == 'number') {
if (dir > 0) {
i = idx >= 0 ? idx : Math.max(idx + length, i);
} else {
length = idx >= 0 ? Math.min(idx + 1, length) : idx + length + 1;
}
} else if (sortedIndex && idx && length) {
idx = sortedIndex(array, item);
return array[idx] === item ? idx : -1;
}
if (item !== item) {
idx = predicateFind(slice.call(array, i, length), _.isNaN);
return idx >= 0 ? idx + i : -1;
}
for (idx = dir > 0 ? i : length - 1; idx >= 0 && idx < length; idx += dir) {
if (array[idx] === item) return idx;
}
return -1;
};
};
// Return the position of the first occurrence of an item in an array,
// or -1 if the item is not included in the array.
// If the array is large and already in sort order, pass `true`
// for **isSorted** to use binary search.
_.indexOf = createIndexFinder(1, _.findIndex, _.sortedIndex);
_.lastIndexOf = createIndexFinder(-1, _.findLastIndex);
// Generate an integer Array containing an arithmetic progression. A port of
// the native Python `range()` function. See
// [the Python documentation](http://docs.python.org/library/functions.html#range).
_.range = function(start, stop, step) {
if (stop == null) {
stop = start || 0;
start = 0;
}
if (!step) {
step = stop < start ? -1 : 1;
}
var length = Math.max(Math.ceil((stop - start) / step), 0);
var range = Array(length);
for (var idx = 0; idx < length; idx++, start += step) {
range[idx] = start;
}
return range;
};
// Chunk a single array into multiple arrays, each containing `count` or fewer
// items.
_.chunk = function(array, count) {
if (count == null || count < 1) return [];
var result = [];
var i = 0, length = array.length;
while (i < length) {
result.push(slice.call(array, i, i += count));
}
return result;
};
// Function (ahem) Functions
// ------------------
// Determines whether to execute a function as a constructor
// or a normal function with the provided arguments.
var executeBound = function(sourceFunc, boundFunc, context, callingContext, args) {
if (!(callingContext instanceof boundFunc)) return sourceFunc.apply(context, args);
var self = baseCreate(sourceFunc.prototype);
var result = sourceFunc.apply(self, args);
if (_.isObject(result)) return result;
return self;
};
// Create a function bound to a given object (assigning `this`, and arguments,
// optionally). Delegates to **ECMAScript 5**'s native `Function.bind` if
// available.
_.bind = restArguments(function(func, context, args) {
if (!_.isFunction(func)) throw new TypeError('Bind must be called on a function');
var bound = restArguments(function(callArgs) {
return executeBound(func, bound, context, this, args.concat(callArgs));
});
return bound;
});
// Partially apply a function by creating a version that has had some of its
// arguments pre-filled, without changing its dynamic `this` context. _ acts
// as a placeholder by default, allowing any combination of arguments to be
// pre-filled. Set `_.partial.placeholder` for a custom placeholder argument.
_.partial = restArguments(function(func, boundArgs) {
var placeholder = _.partial.placeholder;
var bound = function() {
var position = 0, length = boundArgs.length;
var args = Array(length);
for (var i = 0; i < length; i++) {
args[i] = boundArgs[i] === placeholder ? arguments[position++] : boundArgs[i];
}
while (position < arguments.length) args.push(arguments[position++]);
return executeBound(func, bound, this, this, args);
};
return bound;
});
_.partial.placeholder = _;
// Bind a number of an object's methods to that object. Remaining arguments
// are the method names to be bound. Useful for ensuring that all callbacks
// defined on an object belong to it.
_.bindAll = restArguments(function(obj, keys) {
keys = flatten(keys, false, false);
var index = keys.length;
if (index < 1) throw new Error('bindAll must be passed function names');
while (index--) {
var key = keys[index];
obj[key] = _.bind(obj[key], obj);
}
});
// Memoize an expensive function by storing its results.
_.memoize = function(func, hasher) {
var memoize = function(key) {
var cache = memoize.cache;
var address = '' + (hasher ? hasher.apply(this, arguments) : key);
if (!has(cache, address)) cache[address] = func.apply(this, arguments);
return cache[address];
};
memoize.cache = {};
return memoize;
};
// Delays a function for the given number of milliseconds, and then calls
// it with the arguments supplied.
_.delay = restArguments(function(func, wait, args) {
return setTimeout(function() {
return func.apply(null, args);
}, wait);
});
// Defers a function, scheduling it to run after the current call stack has
// cleared.
_.defer = _.partial(_.delay, _, 1);
// Returns a function, that, when invoked, will only be triggered at most once
// during a given window of time. Normally, the throttled function will run
// as much as it can, without ever going more than once per `wait` duration;
// but if you'd like to disable the execution on the leading edge, pass
// `{leading: false}`. To disable execution on the trailing edge, ditto.
_.throttle = function(func, wait, options) {
var timeout, context, args, result;
var previous = 0;
if (!options) options = {};
var later = function() {
previous = options.leading === false ? 0 : _.now();
timeout = null;
result = func.apply(context, args);
if (!timeout) context = args = null;
};
var throttled = function() {
var now = _.now();
if (!previous && options.leading === false) previous = now;
var remaining = wait - (now - previous);
context = this;
args = arguments;
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
result = func.apply(context, args);
if (!timeout) context = args = null;
} else if (!timeout && options.trailing !== false) {
timeout = setTimeout(later, remaining);
}
return result;
};
throttled.cancel = function() {
clearTimeout(timeout);
previous = 0;
timeout = context = args = null;
};
return throttled;
};
// Returns a function, that, as long as it continues to be invoked, will not
// be triggered. The function will be called after it stops being called for
// N milliseconds. If `immediate` is passed, trigger the function on the
// leading edge, instead of the trailing.
_.debounce = function(func, wait, immediate) {
var timeout, result;
var later = function(context, args) {
timeout = null;
if (args) result = func.apply(context, args);
};
var debounced = restArguments(function(args) {
if (timeout) clearTimeout(timeout);
if (immediate) {
var callNow = !timeout;
timeout = setTimeout(later, wait);
if (callNow) result = func.apply(this, args);
} else {
timeout = _.delay(later, wait, this, args);
}
return result;
});
debounced.cancel = function() {
clearTimeout(timeout);
timeout = null;
};
return debounced;
};
// Returns the first function passed as an argument to the second,
// allowing you to adjust arguments, run code before and after, and
// conditionally execute the original function.
_.wrap = function(func, wrapper) {
return _.partial(wrapper, func);
};
// Returns a negated version of the passed-in predicate.
_.negate = function(predicate) {
return function() {
return !predicate.apply(this, argumen