@nichathan-gaming/map-generator
Version:
Creates and generates a 2 dimensional array with various path generation functions.
798 lines (657 loc) • 26.9 kB
JavaScript
import getAStarPath from './getAStarPath/getAStarPath.js';
import generatedTypes from './helpers/generatedTypes/generatedTypes.js';
import shuffle from './helpers/shuffle.js';
import crawlingGenerator from './crawlingGenerator/crawlingGenerator.js';
import recursiveGenerator from './recursiveGenerator/recursiveGenerator.js';
import wilsonsGenerator from './wilsonsGenerator/wilsonsGenerator.js';
import primsGenerator from './primsGenerator/primsGenerator.js';
import fillHoles from './helpers/fillHoles/fillHoles.js';
import assertIndex from './helpers/assertIndex.js';
import tileSelector from './helpers/tileSelector.js';
import { assertType, basicTypes, assertArray } from '@nichathan-gaming/assertions';
import assertGeneratedType from './helpers/generatedTypes/assertGeneratedType.js';
import random from './helpers/random.js';
/**
* A typical mapGenerator generates and manages a 2D array.
*/
class mapGenerator{
generatedType = generatedTypes.none;
/**
* Creates a new mapGenerator
* @param {number} width
* @param {number} height
* @param {T} walkableValue
* @param {T} unwalkableValue
* @param {(T, T)=>boolean} equalityFunction
* @param {*} seed The seed to get a random number at
*/
constructor(width, height, walkableValue, unwalkableValue, equalityFunction, seed){
assertType(width, basicTypes.number);
assertType(height, basicTypes.number);
//set the seed for the random generator even if it is null
random.setSeed(seed);
//set the type that can be used
this.basicType = typeof walkableValue;
//create the 2D array
this.multArray = [...Array(height)].map(_=>[...Array(width)].map(()=>walkableValue));
//set the values
this.setUnwalkableValue(unwalkableValue);
this.setWalkableValue(walkableValue);
//set the equlityFunction
this.changeEqualityFunction(equalityFunction);
//set the width and height
this.width = width;
this.height = height;
};
//#region Getters
/**
* Searches the 8 cells around an index to create an array of the neighbors that do and don't match the pathingValue
* @param {[number, number]} index The index to count at
* @param {T} pathingValue The value to compare to
* @param {boolean} isOffMapPathed When the path meets the edge of the map do the parts off of the map count as walls or not
* @returns {number[][]} an array of the index of nonValues and sameValues
*/
getNeighborsForPath(index, pathingValue, isOffMapPathed){
this.isValidIndex(index);
this.assertBasicType(pathingValue);
//if this is not the pathingValue, return empty arrays
if(!this.isValueAtIndexEqualToValue(index, pathingValue)){
return [[], []];
};
//define the value trackers
const nonValues = [];
const sameValues = [];
//define the search indexes
const searchIndexes = {
'one': [index[0] - 1, index[1] - 1],
'two': [index[0] - 1, index[1]],
'three': [index[0] - 1, index[1] + 1],
'four': [index[0], index[1] - 1],
'five': [index[0], index[1] + 1],
'six': [index[0] + 1, index[1] - 1],
'seven': [index[0] + 1, index[1]],
'eight': [index[0] + 1, index[1] + 1],
};
//define a function to check and categorize indexes
const checkAndCategorize = (indexKey, value) => {
if(this.isValidIndex(searchIndexes[indexKey])){
if(this.isValueAtIndexEqualToValue(searchIndexes[indexKey], pathingValue)){
sameValues.push(value);
}else{
nonValues.push(value);
};
}else{
if(isOffMapPathed){
sameValues.push(value);
}else{
nonValues.push(value);
};
};
};
//get cardinal values at 2, 4, 5, 7
checkAndCategorize('two', 2);
checkAndCategorize('four', 4);
checkAndCategorize('five', 5);
checkAndCategorize('seven', 7);
//if the 2 cardinal directions around the corner are in nonValues, check the corner
if(!(nonValues.includes(2) || nonValues.includes(5))){
checkAndCategorize('three', 3);
};
if(!(nonValues.includes(2) || nonValues.includes(4))){
checkAndCategorize('one', 1);
};
if(!(nonValues.includes(4) || nonValues.includes(7))){
checkAndCategorize('six', 6);
};
if(!(nonValues.includes(7) || nonValues.includes(5))){
checkAndCategorize('eight', 8);
};
//sort the values so they match the strings before returning them
nonValues.sort();
sameValues.sort();
return [
nonValues,
sameValues
];
};
/**
* Determines whether or not the values at the given indexes are equal using the provided equalityFunction
* @param {[number, number]} indexA The first index
* @param {[number, number]} indexB The second index
* @returns {boolean} Whether or not the 2 values are equal
*/
areValuesAtIndexesEqual(indexA, indexB){
//are the indexes valid
assertIndex(indexA);
assertIndex(indexB);
//get the values
const valueA = this.getValueAtIndex(indexA);
const valueB = this.getValueAtIndex(indexB);
return this.areValuesEqual(valueA, valueB);
};
/**
* Determines whether or not the value at the given index and value are equal using the provided equalityFunction
* @param {[number, number]} index The index to get a value at
* @param {T} value The value to compare with
* @returns {boolean} Whether or not the 2 values are equal
*/
isValueAtIndexEqualToValue(index, value){
//ensure the values are valid
assertIndex(index);
this.assertBasicType(value);
//get the value at the index
const foundValue = this.getValueAtIndex(index);
return this.areValuesEqual(foundValue, value);
};
/**
* Determines whether or not valueA and valueB are equal using the provided equalityFunction
* @param {T} valueA The first value to compare
* @param {T} valueB The second value to compare
* @returns {boolean} Whether or not the 2 values are equal
*/
areValuesEqual(valueA, valueB){
this.assertBasicType(valueA);
this.assertBasicType(valueB);
return this.equalityFunction(valueA, valueB);
};
/**
* Determines if the value at the given index is equal to the unwalkableValue
* @param {[number, number]} index The index to compare with
* @returns {boolean} Whether or not the value at the given index is unwalkable
*/
isIndexUnwalkable(index){
assertIndex(index);
return this.isValueAtIndexEqualToValue(index, this.unwalkableValue);
};
/**
* Determines if a value is equal to the unwalkableValue
* @param {T} value The value to compare with unwalkableValue
* @returns {boolean} If the given value is the same as the unwalkableValue
*/
isValueUnwalkable(value){
this.assertBasicType(value);
return this.areValuesEqual(value, this.unwalkableValue);
};
/**
* Determines if the value at the given index is equal to the walkableValue
* @param {[number, number]} index The index to compare with
* @returns {boolean} Whether or not the value at the given index is walkable
*/
isIndexWalkable(index){
assertIndex(index);
return this.isValueAtIndexEqualToValue(index, this.walkableValue);
};
/**
* Determines if a value is equal to the walkableValue
* @param {T} value The value to compare with walkableValue
* @returns {boolean} If the given value is the same as the walkableValue
*/
isValueWalkable(value){
this.assertBasicType(value);
return this.areValuesEqual(value, this.walkableValue);
};
/**
* Gets the current multArray for this mapGenerator
* @returns This multArray
*/
getMultArray = () => this.multArray;
/**
* Gets the width for this mapGenerator
* @returns this.width
*/
getWidth = () => this.width;
/**
* Gets the height for this mapGenerator
* @returns this.height
*/
getHeight = () => this.height;
/**
* Gets the walkableValue used to construct this mapGenerator
* @returns {T} the value used in the constructor for this mapGenerator
*/
getWalkableValue = () => this.walkableValue;
/**
* Gets the unwalkableValue used to generate paths
* @returns {T} the value used for unwalkable paths
*/
getUnwalkableValue = () => this.unwalkableValue;
/**
* Determines if the given index is within the bounds of this map
* @param {[number, number]} index The index to search at
* @returns {boolean} If this index is within the bounds of this map or not
*/
isValidIndex(index){
assertIndex(index);
return index[0] >= 0 && index[0] < this.height && index[1] >= 0 && index[1] < this.width;
};
/**
* Gets a value at the index.
* Will throw an error if the index is not valid
* @param {[number, number]} index The index to get a value at
* @returns {T} The value at the index
*/
getValueAtIndex(index){
assertIndex(index);
return this.multArray[index[0]][index[1]];
};
/**
* Gets and array of the indexes with the given value
* @param {T} value The value to search for
* @returns {Index[]} An array of indexes with the given value
*/
getAllIndexesForValue(value){
this.assertBasicType(value);
const indexes = [];
for(let i = 0; i < this.height; i++){
for(let j = 0; j < this.width; j++){
const index = [i, j];
if(this.equalityFunction(this.getValueAtIndex(index), value)) indexes.push(index);
};
};
return indexes;
};
/**
* Gets an array of the indexes with the walkableValue
* @returns {index[]} An array of indexes
*/
getWalkableIndexes(){
return this.getAllIndexesForValue(this.walkableValue);
};
/**
* Gets an array of the indexes with the unwalkableValue
* @returns {Index[]} An array of indexes
*/
getUnwalkableIndexes(){
return this.getAllIndexesForValue(this.unwalkableValue);
};
/**
* Finds a path between 2 indexes
* @param {[number, number]} startingIndex The index to start the search at
* @param {[number, number]} endingIndex The index to end the search at
* @returns {Index[]} the path between the 2 indexes or null if a path is not available
*/
getPath(startingIndex, endingIndex){
assertIndex(startingIndex);
assertIndex(endingIndex);
return getAStarPath(this, startingIndex, endingIndex);
};
//#endregion
//#region Setters
/**
* Changes the current generatedType
* @param {generatedTypes} generatedType The type used for the generation
* @returns {mapGenerator} this mapGenerator
*/
changeGeneratedType(generatedType){
//ensure it is a valid generatedType
assertGeneratedType(generatedType);
//set the generated typ
this.generatedType = generatedType;
return this;
};
/**
* Changes the current equality function
* @param {(T, T)=>boolean} equalityFunction The function used to determine the equality of 2 values
* @returns {mapGenerator} this mapGenerator
*/
changeEqualityFunction(equalityFunction){
assertType(equalityFunction, basicTypes.function);
if(equalityFunction(this.walkableValue, this.unwalkableValue)){
throw new Error('The walkable and unwalkable values cannot be equal. Either change the values or the equalityFunction');
};
this.equalityFunction = equalityFunction;
return this;
};
/**
* Changes the walkableValue for this mapGenerator
* @param {T} newWalkableValue The new value for the path
* @returns {mapGenerator} this mapGenerator
*/
setWalkableValue(newWalkableValue){
this.assertBasicType(newWalkableValue);
this.walkableValue = newWalkableValue;
return this;
};
/**
* Changes the unwalkableValue for this mapGenerator
* @param {T} newUnwalkableValue The new value for unwalkable values
* @returns {mapGenerator} this mapGenerator
*/
setUnwalkableValue(newUnwalkableValue){
this.assertBasicType(newUnwalkableValue);
this.unwalkableValue = newUnwalkableValue;
return this;
};
/**
* Sets the value at the index.
* Will throw an error if the index is not valid
* @param {[number, number]} index The index to set the value at
* @param {T} value The value to set at the index
* @returns {mapGenerator} this mapGenerator
*/
setValueAtIndex(index, value){
assertIndex(index);
this.assertBasicType(value);
if(this.isValidIndex(index)){
this.multArray[index[0]][index[1]] = value;
};
return this;
};
/**
* Sets the value to all valid indexes
* @param {T} value The value to set
* @param {[number, number][]} indexes The indexes to set the value at
* @returns {mapGenerator} this mapGenerator
*/
setValueAtIndexes(value, ...indexes){
this.assertBasicType(value);
indexes.forEach(el=>{
assertIndex(el);
this.setValueAtIndex(el, value);
});
return this;
};
/**
* Sets the value and index for each object
* @param {{value: T, index: [number, number]}[]} indexValues An array of {value, index}
* @returns {mapGenerator} this mapGenerator
*/
setValuesAtIndexes(...indexValues){
assertArray(indexValues);
//go through all indexValues and set them
indexValues.forEach(({index, value})=>{
assertIndex(index);
this.assertBasicType(value);
this.setValueAtIndex(index, value);
});
return this;
};
/**
* If the index is valid, set the value at the index to the walkableValue used to create the map with
* @param {[number, number]} index The index to set the value at
* @returns {mapGenerator} this mapGenerator
*/
setWalkableValueAtIndex(index){
assertIndex(index);
if(this.isValidIndex(index)){
this.setValueAtIndex(index, this.walkableValue);
};
return this;
};
/**
* Sets the walkableValue to all valid indexes
* @param {[number, number][]} indexes The indexes to set as walkable
* @returns {mapGenerator} this mapGenerator
*/
setWalkableValueAtIndexes(...indexes){
assertArray(indexes);
this.setValueAtIndexes(this.walkableValue, ...indexes);
return this;
};
/**
* If the index is valid, set the value at the index to the unwalkableValue used to create the map with
* @param {[number, number]} index The index to set the value at
* @returns {mapGenerator} this mapGenerator
*/
setUnwalkableValueAtIndex(index){
assertIndex(index);
if(this.isValidIndex(index)) this.setValueAtIndex(index, this.unwalkableValue);
return this;
};
/**
* Sets the unwalkableValue to all valid indexes
* @param {[number, number][]} indexes The indexes to set as unwalkable
* @returns {mapGenerator} this mapGenerator
*/
setUnwalkableValueAtIndexes(...indexes){
assertArray(indexes);
this.setValueAtIndexes(this.unwalkableValue, ...indexes);
return this;
};
/**
* Fills the multArray with the given value
* @param {T} value The value to fill this multArray with
* @returns {mapGenerator} this mapGenerator
*/
fillWithValue(value){
this.assertBasicType(value);
this.multArray = [...Array(this.height)].map(_=>[...Array(this.width)].map(()=>value));
return this;
};
//#endregion
//#region Helpers
/**
* Finds the display index for Nichathan Gaming's mapRenderer
* @param {[number, number]} index The index to find the display index at.
* @param {T} pathingValue The value to make the path at.
* @param {boolean} isOffMapPathed When the pathingValue is at the edge of the map do we consider that as a non-pathed value
* @returns {[number, number]} The index of the sprite to display.
*/
findDisplayIndex(index, pathingValue, isOffMapPathed){
const neighbors = this.getNeighborsForPath(index, pathingValue, isOffMapPathed);
return tileSelector(neighbors[0], neighbors[1]);
};
/**
* Adds a border to this mapGenerator.
* @param {number} size The size of the border
* @param {T} value The value to add as the border
* @returns {mapGenerator} mapGenerator
*/
addBorder(size, value){
this.assertBasicType(value);
if(!(size > 1 || Number.isInteger(size))){
throw new Error(`Expected size to be a whole number greater than 1. Found: ${size}.`);
};
this.width += size * 2;
this.height += size * 2;
//add horizontal borders
this.multArray = this.multArray.map(row=>{
const horizontalBorder = new Array(size).fill(value);
return [...horizontalBorder, ...row, ...horizontalBorder];
});
//add vertical borders
const verticalBorder = new Array(this.width).fill(value);
for(let i = 0; i < size; i++){
this.multArray.push(verticalBorder);
this.multArray.unshift(verticalBorder);
};
return this;
};
/**
* Adds a border to this mapGenerator. With the walkableValue for the border.
* @param {number} size The size of the border
* @returns {mapGenerator} mapGenerator
*/
addWalkableBorder(size){
return this.addBorder(size, this.walkableValue);
};
/**
* Adds a border to this mapGenerator. With the unwalkableValue for the border.
* @param {number} size The size of the border
* @returns {mapGenerator} mapGenerator
*/
addUnwalkableBorder(size){
return this.addBorder(size, this.unwalkableValue);
};
/**
* Increases the map by the given amount.
*
* I.E.
* 0 1
* 1 1
* multiplyMap(2)
* 0 0 1 1
* 0 0 1 1
* 1 1 1 1
* 1 1 1 1
* @param {number} amount must be a whole number greater than 1
* @returns {mapGenerator} mapGenerator
*/
multiplyMap(amount){
if(!(amount > 1 || Number.isInteger(amount))){
throw new Error(`Expected amount to be a whole number greater than 1. Found: ${amount}.`);
};
this.multArray = this.multArray.flatMap(row => Array(amount).fill(row.flatMap(col => Array(amount).fill(col))));
this.width *= amount;
this.height *= amount;
return this;
};
/**
* Asserts the value is of the basic type otherwise throws an error
* @param {*} value The value to assert is of the type used to construct this mapGenerator
*/
assertBasicType(value){
assertType(value, this.basicType);
};
/**
* logs this multArray to the console with table
* @returns {mapGenerator} this mapGenerator
*/
logMap(){
console.log(this.generatedType);
console.table(this.multArray);
return this;
};
/**
* Shuffles the given array
* @param {[]} array The array to shuffle
* @param {*} seed The seed to get a random number at
* @returns {[]} The array with elements in random locations
*/
static shuffleArray(array, seed){
assertArray(array);
return shuffle(array, seed);
};
//#endregion
//#region generators
/**
* Places a value randomly across the map
* @param {number} randomChance The chance from 0 to 0.9999 that a value will be unwalkable
* @param {*} seed The seed to get a random number at
* @returns {mapGenerator} this mapGenerator
*/
generateRandomly(randomChance, seed){
assertType(randomChance, basicTypes.number);
//set the new seed
random.setSeed(seed);
//reset this map
this.fillWithValue(this.walkableValue);
//set the generated type
this.generatedType = generatedTypes.random;
//give each element a randomChance chance to be set
for(let i = 0; i < this.height; i++){
for(let j = 0; j < this.width; j++){
if(random.getNext() < randomChance) {
this.setValueAtIndex([i, j], this.unwalkableValue);
};
};
};
return this;
};
/**
* Crawls around the map creating a path.
* @param {number} verticalCrawlCount The number of top to bottom crawls
* @param {number} horizontalCrawlCount The number of left to right crawls
* @param {*} seed The seed to get a random number at
* @returns {mapGenerator} this mapGenerator
*/
generateCrawler(verticalCrawlCount, horizontalCrawlCount, seed){
assertType(verticalCrawlCount, basicTypes.number);
assertType(horizontalCrawlCount, basicTypes.number);
//set the new seed
random.setSeed(seed);
//fill array with unwalkable value
this.fillWithValue(this.walkableValue);
//set the generated type
this.generatedType = generatedTypes.crawl;
//run the crawler
crawlingGenerator(this, verticalCrawlCount, horizontalCrawlCount);
return this;
};
/**
* Recursively generates a path
* @param {[number, number]} startIndex The index to begin at
* @param {number} maxPathSize The maximum size for the path
* @param {boolean} shouldFillHoles If the generator should attempt to fill holes at the end
* @param {*} seed The seed to get a random number at
* @returns {mapGenerator} this mapGenerator
*/
generateRecursively(startIndex, maxPathSize, shouldFillHoles, seed){
assertIndex(startIndex);
assertType(maxPathSize, basicTypes.number);
assertType(shouldFillHoles, basicTypes.boolean);
//set the new seed
random.setSeed(seed);
//fill array with unwalkable value
this.fillWithValue(this.unwalkableValue);
//set the generated type
this.generatedType = generatedTypes.recursive;
//run the generator
recursiveGenerator(this, maxPathSize, startIndex);
//try to fill more holes in the map
if(shouldFillHoles){
fillHoles(this, maxPathSize);
};
return this;
};
/**
* Generates a path following the Wilson's algorithm
* @param {[number, number]} startIndex The index to begin the path at
* @param {number} maxPathSize The maximum size for a path (not length)
* @param {T} possiblePathValue A third value used to hold the place of a possible path
* @param {boolean} shouldFillHoles If the generator should attempt to fill holes at the end
* @param {*} seed The seed to get a random number at
* @returns {mapGenerator} this mapGenerator
*/
generateWilsons(startIndex, maxPathSize, possiblePathValue, shouldFillHoles, seed){
assertIndex(startIndex);
assertType(maxPathSize, basicTypes.number);
this.assertBasicType(possiblePathValue);
assertType(shouldFillHoles, basicTypes.boolean);
//set the new seed
random.setSeed(seed);
//don't even search for a path if either of our path values are equal
if(this.isValueWalkable(possiblePathValue) || this.isValueUnwalkable(possiblePathValue)){
throw new Error(`Neither the unwalkableValue, possiblePathValue nor the map walkableValue can be equal.`);
};
//fill array with unwalkable value
this.fillWithValue(this.unwalkableValue);
//set the generated type
this.generatedType = generatedTypes.wilsons;
//run the generator
wilsonsGenerator(this, maxPathSize, startIndex, possiblePathValue);
//try to fill more holes in the map
if(shouldFillHoles){
fillHoles(this, maxPathSize);
};
return this;
};
/**
* Generates a path following the Prim's algorithm
* @param {[number, number]} startIndex The index to begin the path at
* @param {number} maxPathSize The maximum size for a path (not length)
* @param {boolean} shouldFillHoles If the generator should attempt to fill holes at the end
* @param {*} seed The seed to get a random number at
* @returns {mapGenerator} this mapGenerator
*/
generatePrims(startIndex, maxPathSize, shouldFillHoles, seed){
assertIndex(startIndex);
assertType(maxPathSize, basicTypes.number);
assertType(shouldFillHoles, basicTypes.boolean);
//set the new seed
random.setSeed(seed);
//fill array with unwalkable value
this.fillWithValue(this.unwalkableValue);
//set the generated type
this.generatedType = generatedTypes.prims;
//run the generator
primsGenerator(this, maxPathSize, startIndex);
//try to fill more holes in the map
if(shouldFillHoles){
fillHoles(this, maxPathSize);
};
return this;
};
//#endregion
};
export default mapGenerator;