UNPKG

@nichathan-gaming/map-generator

Version:

Creates and generates a 2 dimensional array with various path generation functions.

798 lines (657 loc) 26.9 kB
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;