isaacscript-common
Version:
Helper functions and features for IsaacScript mods.
568 lines (567 loc) • 21.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.arrayEquals = arrayEquals;
exports.arrayRemove = arrayRemove;
exports.arrayRemoveAllInPlace = arrayRemoveAllInPlace;
exports.arrayRemoveInPlace = arrayRemoveInPlace;
exports.arrayRemoveIndex = arrayRemoveIndex;
exports.arrayRemoveIndexInPlace = arrayRemoveIndexInPlace;
exports.arrayToString = arrayToString;
exports.combineArrays = combineArrays;
exports.copyArray = copyArray;
exports.emptyArray = emptyArray;
exports.filterMap = filterMap;
exports.getArrayCombinations = getArrayCombinations;
exports.getArrayDuplicateElements = getArrayDuplicateElements;
exports.getArrayIndexes = getArrayIndexes;
exports.getHighestArrayElement = getHighestArrayElement;
exports.getLowestArrayElement = getLowestArrayElement;
exports.getRandomArrayElement = getRandomArrayElement;
exports.getRandomArrayElementAndRemove = getRandomArrayElementAndRemove;
exports.getRandomArrayIndex = getRandomArrayIndex;
exports.includes = includes;
exports.isArray = isArray;
exports.isArrayContiguous = isArrayContiguous;
exports.isArrayElementsUnique = isArrayElementsUnique;
exports.isArrayInArray = isArrayInArray;
exports.setAllArrayElements = setAllArrayElements;
exports.shuffleArray = shuffleArray;
exports.shuffleArrayInPlace = shuffleArrayInPlace;
exports.sumArray = sumArray;
exports.swapArrayElements = swapArrayElements;
const ReadonlySet_1 = require("../types/ReadonlySet");
const random_1 = require("./random");
const rng_1 = require("./rng");
const sort_1 = require("./sort");
const types_1 = require("./types");
const utils_1 = require("./utils");
/**
* Helper function for determining if two arrays contain the exact same elements. Note that this
* only performs a shallow comparison.
*/
function arrayEquals(array1, array2) {
if (array1.length !== array2.length) {
return false;
}
return array1.every((array1Element, i) => {
const array2Element = array2[i];
return array1Element === array2Element;
});
}
/**
* Builds a new array based on the original array without the specified element(s). Returns the new
* array. If the specified element(s) are not found in the array, it will simply return a shallow
* copy of the array.
*
* If there is more than one matching element in the array, this function will remove all of them.
*
* This function is variadic, meaning that you can specify N arguments to remove N elements.
*/
function arrayRemove(originalArray, ...elementsToRemove
// eslint-disable-next-line complete/no-mutable-return
) {
const elementsToRemoveSet = new ReadonlySet_1.ReadonlySet(elementsToRemove);
const array = [];
for (const element of originalArray) {
if (!elementsToRemoveSet.has(element)) {
array.push(element);
}
}
return array;
}
/**
* Removes all of the specified element(s) from the array. If the specified element(s) are not found
* in the array, this function will do nothing.
*
* This function is variadic, meaning that you can specify N arguments to remove N elements.
*
* If there is more than one matching element in the array, this function will remove every matching
* element. If you want to only remove the first matching element, use the `arrayRemoveInPlace`
* function instead.
*
* @returns True if one or more elements were removed, false otherwise.
*/
function arrayRemoveAllInPlace(
// eslint-disable-next-line complete/prefer-readonly-parameter-types
array, ...elementsToRemove) {
let removedOneOrMoreElements = false;
for (const element of elementsToRemove) {
let index;
do {
index = array.indexOf(element);
if (index > -1) {
removedOneOrMoreElements = true;
array.splice(index, 1);
}
} while (index > -1);
}
return removedOneOrMoreElements;
}
/**
* Removes the specified element(s) from the array. If the specified element(s) are not found in the
* array, this function will do nothing.
*
* This function is variadic, meaning that you can specify N arguments to remove N elements.
*
* If there is more than one matching element in the array, this function will only remove the first
* matching element. If you want to remove all of the elements, use the `arrayRemoveAllInPlace`
* function instead.
*
* @returns The removed elements. This will be an empty array if no elements were removed.
*/
function arrayRemoveInPlace(
// eslint-disable-next-line complete/prefer-readonly-parameter-types
array, ...elementsToRemove
// eslint-disable-next-line complete/no-mutable-return
) {
const removedElements = [];
for (const element of elementsToRemove) {
const index = array.indexOf(element);
if (index !== -1) {
const removedElement = array.splice(index, 1);
removedElements.push(...removedElement);
}
}
return removedElements;
}
/**
* Shallow copies and removes the elements at the specified indexes from the array. Returns the
* copied array. If the specified indexes are not found in the array, it will simply return a
* shallow copy of the array.
*
* This function is variadic, meaning that you can specify N arguments to remove N elements.
*/
function arrayRemoveIndex(originalArray, ...indexesToRemove
// eslint-disable-next-line complete/no-mutable-return
) {
const indexesToRemoveSet = new ReadonlySet_1.ReadonlySet(indexesToRemove);
const array = [];
for (const [i, element] of originalArray.entries()) {
if (!indexesToRemoveSet.has(i)) {
array.push(element);
}
}
return array;
}
/**
* Removes the elements at the specified indexes from the array. If the specified indexes are not
* found in the array, this function will do nothing.
*
* This function is variadic, meaning that you can specify N arguments to remove N elements.
*
* @returns The removed elements. This will be an empty array if no elements were removed.
*/
function arrayRemoveIndexInPlace(
// eslint-disable-next-line complete/prefer-readonly-parameter-types
array, ...indexesToRemove
// eslint-disable-next-line complete/no-mutable-return
) {
const legalIndexes = indexesToRemove.filter((i) => i >= 0 && i < array.length);
if (legalIndexes.length === 0) {
return [];
}
const legalIndexesSet = new ReadonlySet_1.ReadonlySet(legalIndexes);
const removedElements = [];
for (let i = array.length - 1; i >= 0; i--) {
if (legalIndexesSet.has(i)) {
const removedElement = array.splice(i, 1);
removedElements.push(...removedElement);
}
}
return removedElements;
}
function arrayToString(array) {
if (array.length === 0) {
return "[]";
}
const strings = array.map((element) => tostring(element));
const commaSeparatedStrings = strings.join(", ");
return `[${commaSeparatedStrings}]`;
}
/**
* Helper function to combine two or more arrays. Returns a new array that is the composition of all
* of the specified arrays.
*
* This function is variadic, meaning that you can specify N arguments to combine N arrays. Note
* that this will only perform a shallow copy of the array elements.
*/
// eslint-disable-next-line complete/no-mutable-return
function combineArrays(...arrays) {
const elements = [];
for (const array of arrays) {
for (const element of array) {
elements.push(element);
}
}
return elements;
}
/**
* Helper function to perform a shallow copy.
*
* @param oldArray The array to copy.
* @param numElements Optional. If specified, will only copy the first N elements. By default, the
* entire array will be copied.
*/
// eslint-disable-next-line complete/no-mutable-return
function copyArray(oldArray, numElements) {
// Using the spread operator was benchmarked to be faster than manually creating an array using
// the below algorithm.
if (numElements === undefined) {
return [...oldArray];
}
const newArrayWithFirstNElements = [];
for (let i = 0; i < numElements; i++) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
newArrayWithFirstNElements.push(oldArray[i]);
}
return newArrayWithFirstNElements;
}
/** Helper function to remove all of the elements in an array in-place. */
// eslint-disable-next-line complete/prefer-readonly-parameter-types
function emptyArray(array) {
array.splice(0);
}
/**
* Helper function to perform a filter and a map at the same time. Similar to `Array.map`, provide a
* function that transforms a value, but return `undefined` if the value should be skipped. (Thus,
* this function cannot be used in situations where `undefined` can be a valid array element.)
*
* This function is useful because the `Array.map` method will always produce an array with the same
* amount of elements as the original array.
*
* This is named `filterMap` after the Rust function:
* https://doc.rust-lang.org/std/iter/struct.FilterMap.html
*/
function filterMap(array, func) {
const filteredArray = [];
for (const element of array) {
const newElement = func(element);
if (newElement !== undefined) {
filteredArray.push(newElement);
}
}
return filteredArray;
}
/**
* Helper function to get all possible combinations of the given array. This includes the
* combination of an empty array.
*
* For example, if this function is provided an array containing 1, 2, and 3, then it will return an
* array containing the following arrays:
*
* - [] (if `includeEmptyArray` is set to true)
* - [1]
* - [2]
* - [3]
* - [1, 2]
* - [1, 3]
* - [2, 3]
* - [1, 2, 3]
*
* From: https://github.com/firstandthird/combinations/blob/master/index.js
*
* @param array The array to get the combinations of.
* @param includeEmptyArray Whether to include an empty array in the combinations.
* @param min Optional. The minimum number of elements to include in each combination. Default is 1.
* @param max Optional. The maximum number of elements to include in each combination. Default is
* the length of the array.
*/
function getArrayCombinations(array, includeEmptyArray, min, max) {
if (min === undefined || min <= 0) {
min = 1;
}
if (max === undefined || max <= 0) {
max = array.length;
}
const all = [];
for (let i = min; i < array.length; i++) {
addCombinations(i, array, [], all);
}
if (array.length === max) {
all.push(array);
}
// Finally, account for the empty array combination.
if (includeEmptyArray) {
all.unshift([]);
}
return all;
}
/** Mutates the `all` array in-place. */
function addCombinations(n, src, got,
// eslint-disable-next-line complete/prefer-readonly-parameter-types
all) {
if (n === 0) {
if (got.length > 0) {
all[all.length] = got;
}
return;
}
for (const [i, element] of src.entries()) {
addCombinations(n - 1, src.slice(i + 1), [...got, element], all);
}
}
/**
* Helper function to get the duplicate elements in an array. Only one element for each value will
* be returned. The elements will be sorted before they are returned.
*/
function getArrayDuplicateElements(array) {
const duplicateElements = new Set();
const set = new Set();
for (const element of array) {
if (set.has(element)) {
duplicateElements.add(element);
}
set.add(element);
}
const values = [...duplicateElements];
return values.toSorted(sort_1.sortNormal);
}
/**
* Helper function to get an array containing the indexes of an array.
*
* For example, an array of `["Apple", "Banana"]` would return an array of `[0, 1]`.
*
* Note that normally, you would use the `Object.keys` method to get the indexes of an array, but
* due to implementation details of TypeScriptToLua, this results in an array of 1 through N
* (instead of an array of 0 through N -1).
*/
function getArrayIndexes(array) {
return (0, utils_1.eRange)(array.length);
}
/**
* Helper function to get the highest value in an array. Returns undefined if there were no elements
* in the array.
*/
function getHighestArrayElement(array) {
if (array.length === 0) {
return undefined;
}
let highestValue;
for (const element of array) {
if (highestValue === undefined || element > highestValue) {
highestValue = element;
}
}
return highestValue;
}
/**
* Helper function to get the lowest value in an array. Returns undefined if there were no elements
* in the array.
*/
function getLowestArrayElement(array) {
if (array.length === 0) {
return undefined;
}
let lowestValue;
for (const element of array) {
if (lowestValue === undefined || element < lowestValue) {
lowestValue = element;
}
}
return lowestValue;
}
/**
* Helper function to get a random element from the provided array.
*
* If you want to get an unseeded element, you must explicitly pass `undefined` to the `seedOrRNG`
* parameter.
*
* @param array The array to get an element from.
* @param seedOrRNG The `Seed` or `RNG` object to use. If an `RNG` object is provided, the
* `RNG.Next` method will be called. If `undefined` is provided, it will default to
* a random seed.
* @param exceptions Optional. An array of elements to skip over if selected.
*/
function getRandomArrayElement(array, seedOrRNG, exceptions = []) {
if (array.length === 0) {
error("Failed to get a random array element since the provided array is empty.");
}
const arrayToUse = exceptions.length > 0 ? arrayRemove(array, ...exceptions) : array;
const randomIndex = getRandomArrayIndex(arrayToUse, seedOrRNG);
const randomElement = arrayToUse[randomIndex];
(0, utils_1.assertDefined)(randomElement, `Failed to get a random array element since the random index of ${randomIndex} was not valid.`);
return randomElement;
}
/**
* Helper function to get a random element from the provided array. Once the random element is
* decided, it is then removed from the array (in-place).
*
* If you want to get an unseeded element, you must explicitly pass `undefined` to the `seedOrRNG`
* parameter.
*
* @param array The array to get an element from.
* @param seedOrRNG The `Seed` or `RNG` object to use. If an `RNG` object is provided, the
* `RNG.Next` method will be called. If `undefined` is provided, it will default to
* a random seed.
* @param exceptions Optional. An array of elements to skip over if selected.
*/
function getRandomArrayElementAndRemove(
// eslint-disable-next-line complete/prefer-readonly-parameter-types
array, seedOrRNG, exceptions = []) {
const randomArrayElement = getRandomArrayElement(array, seedOrRNG, exceptions);
arrayRemoveInPlace(array, randomArrayElement);
return randomArrayElement;
}
/**
* Helper function to get a random index from the provided array.
*
* If you want to get an unseeded index, you must explicitly pass `undefined` to the `seedOrRNG`
* parameter.
*
* @param array The array to get the index from.
* @param seedOrRNG The `Seed` or `RNG` object to use. If an `RNG` object is provided, the
* `RNG.Next` method will be called. If `undefined` is provided, it will default to
* a random seed.
* @param exceptions Optional. An array of indexes that will be skipped over when getting the random
* index. Default is an empty array.
*/
function getRandomArrayIndex(array, seedOrRNG, exceptions = []) {
if (array.length === 0) {
error("Failed to get a random array index since the provided array is empty.");
}
return (0, random_1.getRandomInt)(0, array.length - 1, seedOrRNG, exceptions);
}
/**
* Similar to the `Array.includes` method, but works on a widened version of the array.
*
* This is useful when the normal `Array.includes` produces a type error from an array that uses an
* `as const` assertion.
*/
function includes(array, searchElement) {
const widenedArray = array;
return widenedArray.includes(searchElement);
}
/**
* Since Lua uses tables for every non-primitive data structure, it is non-trivial to determine if a
* particular table is being used as an array. `isArray` returns true if:
*
* - the table contains all numerical indexes that are contiguous, starting at 1
* - the table has no keys (i.e. an "empty" table)
*
* @param object The object to analyze.
* @param ensureContiguousValues Optional. Whether the Lua table has to have all contiguous keys in
* order to be considered an array. Default is true.
*/
function isArray(object, ensureContiguousValues = true) {
if (!(0, types_1.isTable)(object)) {
return false;
}
// First, if there is a metatable, this cannot be a simple array and must be a more complex
// object.
const metatable = getmetatable(object);
if (metatable !== undefined) {
return false;
}
// Second, handle the case of an "empty" table.
const keys = Object.keys(object);
if (keys.length === 0) {
return true;
}
// Third, handle the case of non-numerical keys.
const hasAllNumberKeys = keys.every((key) => (0, types_1.isNumber)(key));
if (!hasAllNumberKeys) {
return false;
}
// Fourth, check for non-contiguous elements. (Lua tables start at an index of 1.)
if (ensureContiguousValues) {
for (let i = 1; i <= keys.length; i++) {
const element = object.get(i);
if (element === undefined) {
return false;
}
}
}
return true;
}
/**
* Helper function to see if every element in the array is N + 1.
*
* For example, `[2, 3, 4]` would return true, and `[2, 3, 5]` would return false.
*/
function isArrayContiguous(array) {
let lastValue;
for (const element of array) {
lastValue ??= element - 1;
if (element !== lastValue - 1) {
return false;
}
}
return true;
}
/**
* Helper function to check if all the elements of an array are unique within that array.
*
* Under the hood, this is performed by converting the array to a set.
*/
function isArrayElementsUnique(array) {
const set = new Set(array);
return set.size === array.length;
}
/** Checks if an array is in the provided 2-dimensional array. */
function isArrayInArray(arrayToMatch, parentArray) {
return parentArray.some((element) => arrayEquals(element, arrayToMatch));
}
/** Helper function to set every element in an array to a specific value. */
// eslint-disable-next-line complete/prefer-readonly-parameter-types
function setAllArrayElements(array, value) {
for (let i = 0; i < array.length; i++) {
array[i] = value;
}
}
/**
* Shallow copies and shuffles the array using the Fisher-Yates algorithm. Returns the copied array.
*
* If you want an unseeded shuffle, you must explicitly pass `undefined` to the `seedOrRNG`
* parameter.
*
* From: https://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array
*
* @param originalArray The array to shuffle.
* @param seedOrRNG The `Seed` or `RNG` object to use. If an `RNG` object is provided, the
* `RNG.Next` method will be called. If `undefined` is provided, it will default to
* a random seed.
*/
function shuffleArray(originalArray, seedOrRNG) {
const array = copyArray(originalArray);
shuffleArrayInPlace(array, seedOrRNG);
return array;
}
/**
* Shuffles the provided array in-place using the Fisher-Yates algorithm.
*
* If you want an unseeded shuffle, you must explicitly pass `undefined` to the `seedOrRNG`
* parameter.
*
* From: https://stackoverflow.com/questions/2450954/how-to-randomize-shuffle-a-javascript-array
*
* @param array The array to shuffle.
* @param seedOrRNG The `Seed` or `RNG` object to use. If an `RNG` object is provided, the
* `RNG.Next` method will be called. If `undefined` is provided, it will default to
* a random seed.
*/
function shuffleArrayInPlace(
// eslint-disable-next-line complete/prefer-readonly-parameter-types
array, seedOrRNG) {
let currentIndex = array.length;
const rng = (0, rng_1.isRNG)(seedOrRNG) ? seedOrRNG : (0, rng_1.newRNG)(seedOrRNG);
while (currentIndex > 0) {
currentIndex--;
const randomIndex = getRandomArrayIndex(array, rng);
swapArrayElements(array, currentIndex, randomIndex);
}
}
/** Helper function to sum every value in an array together. */
function sumArray(array) {
return array.reduce((accumulator, element) => accumulator + element, 0);
}
/**
* Helper function to swap two different array elements. (The elements will be swapped in-place.)
*/
function swapArrayElements(
// eslint-disable-next-line complete/prefer-readonly-parameter-types
array, i, j) {
const value1 = array[i]; // eslint-disable-line @typescript-eslint/no-non-null-assertion
const value2 = array[j]; // eslint-disable-line @typescript-eslint/no-non-null-assertion
array[i] = value2;
array[j] = value1;
}