Javascript Snake Game - too much recursion error - javascript
so I'm working on this snake game, and I'm basically trying to prevent the food from spawning on top of the snake tail. My setup variables:
let headX = 10; //snake starting position
let headY = 10;
let appleX = 5; //food starting position
let appleY = 5;
This is the function that checks head/food collision
function checkAppleCollision() {
if (appleX === headX && appleY === headY) {
generateApplePosition();
tailLength++;
score++;
}
}
And this is the function that randomizes the apple position after collision, and also returns the "too much recursion" error, after a couple of collisions:
function generateApplePosition() {
let collisionDetect = false;
let newAppleX = Math.floor(Math.random() * tileCount);
let newAppleY = Math.floor(Math.random() * tileCount);
for (let i = 0; i < snakeTail.length; i++) {
let segment = snakeTail[i];
if (newAppleX === segment.x && newAppleY === segment.y) {
collisionDetect = true;
}
}
while (collisionDetect === true) {
generateApplePosition();
}
appleX = newAppleX;
appleY = newAppleY;
}
Please help, I have no idea what to do here. Everything else works as intended.
Using recursions or do while is a bad idea (I'll explain later)
meanwhile, you could simplify your logic by creating:
reusable samePos() and collides() functions
a recursive createApple() function, which will return itself if the randomly generated x,y positions are occupied by the snake body
const world = {w:6, h:1}; // height set to 1 for this demo only
const snake = [{x:0, y:0}, {x:1, y:0}, {x:2, y:0}, {x:3, y:0}];
const apple = {pos: {x:0, y:0}};
// Check if two object's x,y match
const samePos = (a, b) => a.x === b.x && a.y === b.y;
// Check if object x,y is inside an array of objects
const collides = (ob, arr) => arr.some(o => samePos(ob, o));
const createApple = () => {
const randPos = {
x: ~~(Math.random() * world.w),
y: ~~(Math.random() * world.h),
};
if (collides(randPos, snake)) {
console.log(`position ${randPos.x} ${randPos.y} is occupied by snake`);
return createApple(); // Try another position.
}
// Finally a free spot!
apple.pos = randPos;
console.log(`Apple to free position: ${apple.pos.x} ${apple.pos.y}`);
}
createApple();
Run this demo multiple times
The problem
Useless random guesswork!
As you can see from the example above, if you run it multiple times, very often the randomly generated number is the same as the previously generated one:
...
position 2 0 is occupied by snake <<<
position 1 0 is occupied by snake
position 2 0 is occupied by snake <<<
position 2 0 is occupied by snake <<<
position 1 0 is occupied by snake
position 2 0 is occupied by snake <<<
...
therefore, as your snake grows in size, the recursion might go wild — ad absurdum, iterating way too many times, repeating and failing on the same xy positions, until finally hitting a rare free spot...
This is a really bad design.
Solutions
One solution would be to keep track of the already used randomized positions inside an Array - but that implies unnecessarily to go trough such an Array.
A best solution would be to actually treat the game not as a 2D game, but as a 1D game:
Consider this 2D map of size 4x3 as indexes:
0 1 2 3
4 5 6 7
8 9 10 11
now, let's place a snake into this map:
0 ⬛ 2 3
4 ⬛ ⬛ 7
8 9 ⬛ 11
here's the linear map with the Snake as a 1D list:
[ 0 ⬛ 2 3 4 ⬛ ⬛ 7 8 9 ⬛ 11 ]
therefore, instead of using an array of objects {x:n, y:n} for the snake body positions, all you need is:
[1, 5, 6, 10] // Snake body as indexes
Now that you know all the indexes where you're not allowed to place an Apple, all you need to do when creating the new apple is:
Create an Array of 0-N indexes of length: world.w * world.h
Loop the snake body indexes and delete those indexes from the array of indexes to get an Array of free spots indexes
Simply get only once a random key from that array of free spots!
const indexToXY = (index, width) => ({ x: index%width, y: Math.trunc(index/width) });
const world = {w:4, h:3};
const snakeBody = [1, 5, 6, 10];
const createApple = () => {
const arr = [...Array(world.w * world.h).keys()]; // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]
snakeBody.forEach(i => delete arr[i]);
const freeIndexes = arr.filter(k => k !== undefined); // [0, 2, 3, 4, 7, 8, 9, 11]
const appleIndex = freeIndexes[~~(Math.random() * freeIndexes.length)];
const applePos = indexToXY(appleIndex, world.w);
console.log("New apple position: %o", applePos);
};
createApple();
Run this demo multiple times
Having that free spot index simply draw your apple at the XY coordinates using this simple formula
X = index % mapWidth
Y = floor(index / mapWidth)
As others have said, this doesn't need to be recursive, and you should also take into account the (however unlikely) possibility where there are no more tiles to spawn on which would result in an infinite loop.
function generateApplePosition() {
// Count how many tiles are left for spawning in
const tilesLeft = (tileCount * tileCount) - snakeTail.length;
let collisionDetect;
if (tilesLeft > 0) {
do {
const newAppleX = Math.floor(Math.random() * tileCount);
const newAppleY = Math.floor(Math.random() * tileCount);
collisionDetect = false;
for (let i = 0; i < snakeTail.length; i++) {
const { x, y } = snakeTail[i];
if (newAppleX === x && newAppleY === y) {
collisionDetect = true; // Collision
break;
}
}
if (!collisionDetect) {
// Found spawn point
appleX = newAppleX;
appleY = newAppleY;
}
} while (collisionDetect);
}
}
Related
Generate and sort cartesian coordinates around (0,0) in all quadrants using Javascript's nested array loops
I have an variable obj that has the element count that needs a cartesian coordinate. So I want to generate the following matrix. obj = 9, Square root of obj = 3, 3x3 matrix (-1,1) (0,1) (1,1) (-1,0) (0,0) (1,0) (-1,-1) (0,-1) (1,-1) obj = 25, Square root of obj = 5, 5x5 matrix (-2,2) (-1,2) (0,2) (1,2) (2,2) (-2,1) (-1,1) (0,1) (1,1) (2,1) (-2,0) (-1,0) (0,0) (1,0) (2,0) (-2,-1) (-1,-1) (0,-1) (1,-1) (2,-1) (-2,-2) (-1,-2) (0,-2) (1,-2) (2,-2) obj = 49, Square root of obj = 7, 7x7 matrix (-3,3) (-2,3) (-1,3) (0,3) (1,3) (2,3) (3,3) (-3,2) (-2,2) (-1,2) (0,2) (1,2) (2,2) (3,2) (-3,1) (-2,1) (-1,1) (0,1) (1,1) (2,1) (3,1) (-3,0) (-2,0) (-1,0) (0,0) (1,0) (2,0) (3,0) (-3,-1) (-2,-1) (-1,-1) (0,-1) (1,-1) (2,-1) (3,-1) (-3,-2) (-2,-2) (-1,-2) (0,-2) (1,-2) (2,-2) (3,-2) (-3,-3) (-2,-3) (-1,-3) (0,-3) (1,-3) (2,-3) (3,-3) What I did was hardcoded the first set that is when the obj value is 9 to be created inside a loop, and pushed those in a list called coordinates. All I then did was call the loop by passing the Math.sqrt(obj). Problem: There are missing coordinates, when the obj value is greater than 9. For eg: when the obj value is 49. It would create the adjacent previous element, but it won't create the previous element of the previous element (coordinates like (-1, 3), (1, 3), (-3, 1), (3, 1), (-3, -1), (3, -1), (-1, -3), (1, -3)). This is happening because I hardcoded the logic to create the previous coordinate by subtracting with 1. As the obj value increases the current number of missing coordinates is twice the previous number of missing elements (not sure). I can't seem to figure out a way to create the logic to create the missing elements. Another problem is repeating coordinates. Which happened because I used the logic to create the missing elements wrong. Hard to check if all coordinates are correct when the count (obj) value increases. Note: I would like to know different approaches to create the cartesian coordinates around (0, 0). Apparently all my efforts in building the logic ends up with missing elements or repeating elements. And it is hard to actually check if all the coordinates are correct when the obj values increases. I want to create a cartesian coordinate matrix with any value. Currently I'm stuck with using the squares of odd numbers (I plan to substitute the 0 axis for when the number is less than or greater than squares of odd numbers). Approach ideas/concepts to test: As I'm a beginner in graphics programming, I would like to know better approaches to do this. Also here are some approaches I just came up with. I am not sure if this works yet, but I'll add an update. I could maybe create a cross for just the 0's (x,y) axis. And then try to create the rest of the elements by subtracting or adding to each coordinate in the axis. As there are 4 quadrants, I could create 4 individual loops that creates just that particular quadrant's missing coordinates. (0,1) (-1,0) (0,0) (1,0) (0,-1) Another approach could be like to sort the coordinates and then check to see the distance between 2 adjacent coordinates if it is greater than 1 create a new element, else continue checking. Current Code: My demo code at JSFiddle const speak = 'these are the COORDINATES you are looking for!' // 9, 25, 49, 81, 121 => substitutable values for variable 'obj' const obj = 49 // loop using this variable const coordinates = [] // hardcodes const start = [0,0] const points = [] /* points.push(start) */ /** * FIX!. * * needs to also create coordinates from initial coordinate substracted * by more than 1, currently it gets the previous element by substracting 1, * we need to get previous elements of the previous elements based on number * of elements. */ // creating array from coordinates in all quadrants function demo (n) { // pushing initial coordinates for (let i = 1; i <= Math.sqrt(n); i++) { coordinates.push([-i, i], [i-1, i], [i, i], [-i, i-1], [i-1, i-1], [i, i-1], [-i, -i], [i-1, -i], [i, -i]) for (let j = 3; j < Math.sqrt(n); j++) { coordinates.push([-i, i-j], [i-j, i-j], [i, i-j], [i-j, -i]) } } // pushing missing coordinates /* for (let i = 1; i <= Math.sqrt(n); i++) { coordinates.push([i-2, i], [-i, i-2], [i-2, i-2], [i, i-2]) } */ for (let i = 0; i < obj; i++) { points.push(coordinates[i]) } } demo(obj) // sorting multidimensional array points.sort(function (a, b) { return a[1] - b[1] }) /* // print array as row and column of coordinates for (let x = 0; x < Math.sqrt(obj); x++) { let el = [] for (let i = 0; i < Math.sqrt(obj); i++){ el.push(points[i + Math.sqrt(obj) * x]) } console.log(el) */ }
If I understand you correctly you want to have the coordinates in an order so that the left upper corner is first and right lower corner is last, right? You can try it this way let size = 81, //ie a 7x7 grid, rc = Math.floor(Math.sqrt(size)) //number of rows/columns max = Math.floor(rc / 2), //maximum x and y coordinates min = -1 * max; //minimim x and y coordinates coords = [] //the array of coordinates //as the positive y coordinates should be first, iterate from max down to min for (let y = max; y >= min; y--) //for each row, iterate the x cooridinates from min up to max for (let x = min; x <= max; x++) coords.push([x,y]); for (let i = 0; i < rc; i++) { let row = coords.slice(i*rc, (i+1)*rc); //get one full row of coordinates console.log(row.map(x => formatCoordinate(x)).join("")); //and display it } function formatCoordinate(x) { return "|" + `${x[0]}`.padStart(3, " ") + "/" + `${x[1]}`.padStart(3, " ") + "|" } Another way, is, just put your coordinates in the array in any order, and sort the values afterwards. But you have to sort by x and y coordinate, let size = 81, //ie a 7x7 grid, rc = Math.floor(Math.sqrt(size)) //number of rows/columns max = Math.floor(rc / 2), //maximum x and y coordinates min = -1 * max; //minimim x and y coordinates coords = [] //the array of coordinates //coords will be [[-3, -3], [-3, -2], [-3, -1] ..., [3, 3]] for (let i = min; i <= max; i++) for (let j = min; j <= max; j++) coords.push([i,j]); //sort coords to be [[-3, 3], [-2, 3], [-1, 3], ... [3, -3]] coords.sort((a, b) => { if (a[1] != b[1]) //if y coordinates are different return b[1] - a[1]; //higher y coordinates come first return a[0] - b[0]; //lower x coordinates come firs }) for (let i = 0; i < rc; i++) { let row = coords.slice(i*rc, (i+1)*rc); //get one full row of coordinates console.log(row.map(x => formatCoordinate(x)).join("")); //and display it } function formatCoordinate(x) { return "|" + `${x[0]}`.padStart(3, " ") + "/" + `${x[1]}`.padStart(3, " ") + "|" } Both approaches assume that size is the square of an odd number, but you can of course adapt them any way you want, ie in principle you just need to set min and max to any values you want, and both approaches will create a square of coordinates from [[min/max] ... [max/min]].
How to find all elements between two coordinates in a 2d array
Essentially I'm trying to create the game reversi. To cut it short if you don't know what it is, I have a 8x8 board of squares. There are 2 coordinates and I need to determine all the squares that are between the two coordinates and fill them in. The 2 coordinates are either on the same y, same x or diagonal to each other. Can someone explain the logic behind how I would go about doing something like this? How can I determine the coordinates of all the elements between the 2 coordinates.
You need a simple for loop, starting at one of the coordinates and moving towards the other. let connect = (c1, c2) => { // Determine the distance between c1 & c2 let delta = c1.map((v, i) => c2[i] - v); let distance = Math.max(...delta.map(v => Math.abs(v))); // Determine the unit vector (e.g. [1, -1]) to move each iteration let direction = delta.map(v => v / distance); // Starting at `c1`, iterate for `distance` iterations, moving in `direction` each iteration. return [...Array(distance + 1)].map((_, i) => c1.map((v, j) => v + direction[j] * i)); // Same as above, but exclude `c1` and `c2` from the return array. // return [...Array(distance - 1)].map((_, i) => c1.map((v, j) => v + direction[j] * (i + 1))); }; let c1 = [3, 6]; let c2 = [8, 1]; console.log(connect(c1, c2));
Furthest Building You Can Reach Algorithm - JavaScript
I'm trying to create a solution for the problem below in JavaScript. You are given an integer array heights representing the heights of buildings, some bricks, and some ladders. You start your journey from building 0 and move to the next building by possibly using bricks or ladders. While moving from building i to building i+1 (0-indexed), If the current building's height is greater than or equal to the next building's height, you do not need a ladder or bricks. If the current building's height is less than the next building's height, you can either use one ladder or (h[i+1] - h[i]) bricks. Return the furthest building index (0-indexed) you can reach if you use the given ladders and bricks optimally. Input: heights = [4,2,7,6,9,14,12], bricks = 5, ladders = 1 Output: 4 Explanation: Starting at building 0, you can follow these steps: - Go to building 1 without using ladders nor bricks since 4 >= 2. - Go to building 2 using 5 bricks. You must use either bricks or ladders because 2 < 7. - Go to building 3 without using ladders nor bricks since 7 >= 6. - Go to building 4 using your only ladder. You must use either bricks or ladders because 6 < 9. It is impossible to go beyond building 4 because you do not have any more bricks or ladders. My naive solution: var furthestBuilding = function(heights, bricks, ladders) { let pos = 0; while(pos < heights.length) { if(pos === heights.length - 1) break; if(heights[pos+1] > heights[pos]) { const distance = heights[pos+1] - heights[pos]; if(distance <= bricks) bricks -= distance; else if(ladders > 0) ladders--; else break; } pos++; } return pos; }; It does return the expected result for the example above. However, it fails for the input below. heights = [1,5,1,2,3,4,10000] bricks = 4 ladders = 1 My output is 3, but the solution should be 5. Input that crashes for the recursive solution. heights = [253710,459585,71981,223232,977918,148680,123527,250812,260416,554767,473621,88538,966794,644116,865416,590993,550004,374573,105036,568303,460987,24602,757598,519047,263800,315868,963895,266638,598245,713310,489802,364169,742274,973483,807739,253747,564636,472387,598445,675408,626061,527760,922748,244691,41163,108095,953208,54400,191957,182321,801110,526756,11220,560896,782344,565351,570890,931781,511665,108738,357367,853555,674526,388790,686349,554731,102668,335287,461231,496065,489980,525209,693696,140598,784402,564477,743153,156379,370768,94810,121932,338323,972441,553422,865236,627884,673412,16147,858309,802780,150410,657225,761430,916149,644587,364929,661236,207648,507409,209803,663553,296241,51843,758342,448408,310536,733500,390516,580506,313748,729366,961156,766804,752158,713426,946971,433800,611365,806559,950149,831368,871881,132092,644626,150762,487527,365094,316637,684249,740162,605893,272845,416251,905202,984909,602362,424697,686193,566240,159584,600277,767037,211677,441897,586509,965864,393340,497044,881539,145921,159055,866123,603476,657682,284714,85306,470415,534245,641462,472616,159434,421500,843442,634366,625668,444943,657933,129173,914540,215272,598415,457087,437568,490742,172811,212016,435680,599042,789308,279873,689943,369130,618428,524390,877649,118759,60586,37618,20797,492854,946585,583767,944693,62988,358292,708659,23496,966718,557539,131703,358231,215464,771609,375770,855917,147866,543477,786910,760512,468183,542081,373398,979543,126508,361409,842847,872593,746682,893518,457222,978730,161753,697245,205997,363180,807952,795175,808090,462585,658667,186220,858457,923762,700792,294201,584816,514737,261038,327627,205592,221896,444108,979369,129394,44001,790354,353917,72772,330118,360651,635275,849492,966042,843108,158554,406317,995111,147752,121006,486157,678653,217657,4288,573547,820817,164534,921608,308037,373838,385901,343399,813472,58859,346176,68090,539503,322652,958331,832724,585003,75794,228299,31211,302603,601041,362034,300803,347024,650585,172193,876895,603734,165956,796982,786231,738823,562729,158032,364908,988395,775023,671485,424571,572157,623273,772919,914302,661979,920229,614760,934156,511607,889533,382154,82654,973121,549095,639792,412821,305216,74071,571794,969979,932469,335153,898442,938912,729489,872970,874332,8390,345366,901364,245104,315592,895028,533836,427909,737421,161915,510434,768573,179267,237370,562023,650593,869876,544314,464374,701215,789191,746271,871247,385836,788092,890101,286938,367130,635751,295576,607054,913206,556383,512305,253121,461980,951444,192012,897432,140517,842228,924286,268918,765459,344159,347853,592899,247814,379693,421908,295638,672994,774285,78096,886320,998456,10915,581642,549650,905526,186991,586693,320053,829130,465779,191060,238711,415584,273709,35854,55818,305798,667280,334370,121051,665390,230729,51662,904228,971349,7088,567705,265941,380847,760602,280222,351148,518112,609328,381795,46766,301829,886537,338310,130937,813816,446885,126867,578861,996302,56516,316900,648733,457604,903338,974707,336231,878687,776626,583241,353383,591761,438716,892530,231901,959454,915103,50735,453313,519651,940657,68380,38339,339705,19207,844122,483005,582959,592635,870233,208322,862826,598864,989646,583679,219396,371194,111781,493739,313465,383867,545219,171577,761747,992356,973874,497603,976481,136374,138311,918066,787696,929197,589326,801358,944697,28038,211029,752621,210197,491050,939207,254024,145811,767376,922553,796100,15858,899164,950319,728378,563113,532136,705190,290216,359946,214594,327241,641000,385347,786200,700340,576438,227606,498337,451637,425192,286305,472053,335562,587556,683468,290205,997253,868480,320419,392391,128015,674737,410783,136490,46713,154232,574917,904387,99900,490640,268209,994867,135705,390652,412028,404195,490553,184029,624391,836288,619242,570500,367786,908994,934572,226481,281181,469810,376226,354931,55711,43299,487568,853741,556475,842100,133451,371270,820314,735709,859169,992745,981261,506744,573542,544798,335063,71332,345306,551165,522500,148531,323820,525891,571989,109699,540927,391815,383139,528328,941384,577084,148432,537377,589708,613443,589827,688798,501198,304829,719726,181892,891384,237429,447803,49953,555945,69576,765896,194628,866362,533750,798399,369884,258270,964160,796047,420697,486470,781692,825420,689886,392317,278581,151823,184594,295461,723312,604322,248126,43623,91154,600821,55136,709242,990838,263827,564093,735641,174057,932157,750399,807534,338221,830644,171022,156968,351523,814982,403402,975555,955973,400091,523040,382185,577810,257717,544345,243199,509472,450948,839442,387377,553239,145202,822954,478559,487143,514465,587609,575770,547307,386320,410846,81519,599793,874316,730403,913822,800625,96868,913119,843783,699,767204,432828,496436,348230,767865,455134,266270,324004,863226,758456,66451,431182,641607,514915,522399,164590,335706,829719,724524,981933,812770,192582,880771,71867,704720,691726,761694,868674,964177,287148,124076,155241,535262,856554,108951,453851,597675,592745,32413,774791,750298,66826,876820,567338,699491,336474,60148,776819,430070,546456,564666,776689,886534,68830,749993,157504,933346,39836,417088,481438,30183,515310,764031,876787,321614,765291,466180,941767,877507,658149,60699,413225,849839,376668,689777,491763,712459,5768,608757,161358,554199,132368,464770,89566,309794,430979,979239,62376,354441,582188,947427,569030,430121,826059,562654,461350,901008,191328,484599,615686,859104,366550,140695,229053,282037,289028,296120,883539,980557,365526,143257,658629,730361,683520,101817,442395,50455,199765,137552,653983,47041,102020,308470,523274,447051,345263,967056,525031,506873,170405,995568,977216,83193,279492,376521,946443,847471,845107,321145,866307,523882,135730,824806,927733,605908,580895,177233,443804,914175,905847,661407,483093,518439,789231,66585,447439,14824,861841,89137,913636,194682,166773,212398,259259,160638,435374,941416,140851,311224,54813,155003,595354,742575,668942,77310,96783,217826,211522,116834,391751,922905,730508,225636,265187,995541,329461,244649,951125,322487,140958,608238,511144,410963,335698,228967,487748,382037,261094,363854,557078,539851,519352,364988,444038,284404,730251,828294,608545,188095,466810,46659,673970,142329,93794,167913,30119,116528,592075,810599,14144,445947,51745,236481,878706,838520,310352,112640,612690,663852,546444,818881,868195,573845,390221,254379] bricks = 33671263 ladders = 108
Let's say an "ascent" is a height that is either the first in our list or greater than the previous height, and that the magnitude of an ascent is the delta. Use a min_heap to represent ascents via ladder. Keep track of bricks_left and ladders_left, initialized to your inputs. We will maintain the following as we parse the heights array. The ladders will be used for the steepest ascents we've seen up to the number possible, and the bricks will be used for all other ascents. I.e., as long as ladders_left > 0, we add new ascents to the min_heap and subtract one from ladders_left. If ladder_left = 0, we compare the ascent to the top of our min heap. If the smaller of the two is <= bricks_left we can continue, otherwise we're done. Assuming we can continue: if the smaller of the two is the new ascent, subtract it from bricks_left and continue. If the smaller of the two is the top of the min_heap, remove it, add the new ascent to the min_heap, and subtract the former top of the min_heap from bricks_left. -- edit -- The above algorithm is O(log(ladders) * (ascents - ladders)). If we start with not very many ladders and don't like the complexity of a min_heap for some reason, we can sove this in O(ladders * (ascents - ladders)) by replacing the min_heap with an array, and finding the min by scanning the array when we need to.
Approach by using all ladders first and replace the ladders with the smallest delta first to save bricks. /** * #param {number[]} heights * #param {number} bricks * #param {number} ladders * #return {number} */ var furthestBuilding = function (heights, bricks = 0, ladders = 0) { function insert(array, value) { // part for searching the index, taken from // https://stackoverflow.com/a/21822316/1447675 var low = 0, high = array.length; while (low < high) { const mid = (low + high) >>> 1; if (array[mid] < value) low = mid + 1; else high = mid; } array.splice(low, 0, value); } const ladderSizes = []; for (let i = 0, l = heights.length - 1; i < l; i++) { const delta = heights[i + 1] - heights[i]; if (delta <= 0) continue; if (ladders) { insert(ladderSizes, delta); ladders--; continue; } const small = ladderSizes[0]; if (small <= delta && bricks >= small) { bricks -= ladderSizes.shift(); insert(ladderSizes, delta); continue; } if (bricks >= delta) { bricks -= delta; continue; } return i; } return heights.length - 1; }; console.log(furthestBuilding([4, 2, 7, 6, 9, 14, 12], 5, 1)); console.log(furthestBuilding([1, 5, 1, 2, 3, 4, 10000], 4, 1));
Algorithm to convert a 2d voxel map to line sometimes gets stuck
I have a 2D voxel map for a game, which is a 2D array where 1 means ground and 0 means sky. Example: all 1's in the array (ground) are green boxes The algorithm starts at the leftmost ground voxel that touches the sky (red box in picture). It will explore 8 neighbours of the current position to check if one of them is a ground voxel and also touches a sky voxel. This means it should be added to the groundline. Example of the algorithm working (it's able to go in 'caves' too) On this map it figured it out and returned a line across the ground. In some situations it suddenly stops though, like on this map: After about 10 loops it stopped creating the line. Here's the code, with some explanatory comments in there: voxelToLine() { let voxels = this.voxels.length,//this.voxels is the 2d array lineGround = [], checkedVoxels = [], nowChecking, toCheck = [], otherPaths = [], done = false; for (let y = 1; y < voxels - 1; y++)//sets first coordinate for line if (this.voxels[0][y] && (!this.voxels[0][y - 1] || !this.voxels[1][y] || !this.voxels[0][y + 1])) { lineGround[0] = [0, y / voxels]; nowChecking = [1, y];//search starts from this point } let looped = 0; while (!done) {//continues search untill right side is located, or it got stuk (max 10*voxelmap width loops) toCheck = nowChecking.neighbours(8, (n) => n[0] > 0 && n[0] < voxels - 1);//gets 8 neighbour points around current point, neighbours between 1 and (voxelwidth -1) get returned let foundNew = false; for (let i = 0; i < toCheck.length; i++) {//check every neighbour let x = toCheck[i][0], y = toCheck[i][1], index = y * voxels + x; if (!checkedVoxels.includes(index)) { if (this.voxels[x][y] && (!this.voxels[x][y - 1] || !this.voxels[x + 1][y] || !this.voxels[x - 1][y] || !this.voxels[x][y + 1])) { //if the neighbour is a floor voxel, and touches a skyvoxel this neighbour is added to the line checkedVoxels.push(index); if (foundNew) {//if a valid neighbour is already found, this means there are 2 possible paths from the current point otherPaths.push([x, y]); } else { lineGround.push([x / voxels, y / voxels]); nowChecking = [x, y]; //valid point gets added to the line and currently explored point get updated foundNew = true; } if (x >= voxels) done = true; } } else if (i == toCheck.length - 1 && !foundNew) { if (otherPaths.length > 0) { nowChecking = otherPaths.pop(); //if none of the neighbours are correct an alternative path gets explored foundNew = true; } } } if (!foundNew || looped++ > voxels * 10) { //if it never found a valid neighbour, or it's looped too often break from the whileloop console.log('loops: ', looped); break; } } if (lineGround[0][0] !== 0) lineGround.splice(0, 0, [0, lineGround[0][1]]); if (lineGround[lineGround.length - 1][0] !== 1) lineGround.push([1, lineGround[lineGround.length - 1][1]]); //x=0 and x=1 have to exist, so if they don't exist yet, add them return lineGround; } You can also test it here: game. If you click you remove (set to 0) a few voxels within a radius of where you clicked. Also the line gets recalculated. I'm stuck on this, because I have no idea why the line stops in some situations. All code is here. The relevant file is js/Level.js
There are more problems than the one you raised. I played a bit on your site and there are many patterns where things go wrong. I tried to follow the logic of your code, but got lost in details. So I rewrote most of the code. The main idea is that you should keep record of which direction (slope) you are travelling along the ground in order to know in which order you should look among the neighbours for one that is part of the ground. Let's say the neighbours are numbered as follows, from 0 to 7: +---+---+---+ | 7 | 0 | 1 | +---+---+---+ | 6 | * | 2 | +---+---+---+ | 5 | 4 | 3 | +---+---+---+ The cell marked with * is the last cell you found to be on ground level. Now let's say the previous one found was at 6, then the search among the neighbours should start at 7, then 0, 1, 2, ... 5. The first one that is found to be solid, should be the next cell added to ground level. Another example: if the previous one found was at 4 (we're going upward), then the neighbours should be searched starting at 5, then 6, 7, 0, 1, 2 and 3. The first neighbour that is found to be solid (ground) is the one you want to add to your ground line. This way you will follow every curve, into "caves", upward or downward, left or right. Of course, things can still go weird if you start on an island. But I did not attempt to solve that particular case. I've implemented the above idea in the following version of your method: voxelToLine() { let voxels = this.voxels.length, x, y, i; // neighbors' relative coordinates listed in clockwise order const neighbor = [ [0,-1], [1,-1], [1,0], [1,1], [0,1], [-1,1], [-1,0], [-1,-1] ]; for (y = 0; y < voxels; y++) //sets first coordinate for line. if (this.voxels[0][y]) break; // found ground, don't look further down let lineGround = [[0, y / voxels]]; let [curX, curY] = [0, y]; //search starts here let direction = 0; // upward let looped = 0; do {// Continues search until right side is located, // or it got stuk (max 10*voxelmap width loops) for (i = 0; i < 8; i++) {//check every neighbour, starting at `direction` [x, y] = [curX + neighbor[direction][0], curY + neighbor[direction][1]]; // if we found ground, then pick that cell as the next one on the line if (x>=0 && x<voxels && y>=0 && y<voxels && this.voxels[x][y]) break; direction = (direction + 1) % 8; // turn clockwise to get next neighbour } //if it never found a valid neighbour if (i === 8) break; lineGround.push([x / voxels, y / voxels]); // prepare for next round [curX, curY] = [x, y]; direction = (direction + 5) % 8; } while (looped++ <= voxels*10 && curX < voxels - 1); //x=0 and x=1 have to exist, so if they don't exist yet, add them if (lineGround[0][0] !== 0) lineGround.splice(0, 0, [0, lineGround[0][1]]); if (lineGround[lineGround.length - 1][0] !== 1) lineGround.push([1, lineGround[lineGround.length - 1][1]]); return lineGround; }
Looks like it's skipping over the voxel right below the last legitimate ground voxel because it's already been "checked" (added to the checkedVoxels array). Interestingly, this would prevent your ground path to ever turn 90 degrees (you'll notice your example picture doesn't have such a voxel pattern).
Smoothing out values of an array
If I had an array of numbers such as [3, 5, 0, 8, 4, 2, 6], is there a way to “smooth out” the values so they’re closer to each other and display less variance? I’ve looked into windowing the data using something called the Gaussian function for a 1-dimensional case, which is my array, but am having trouble implementing it. This thread seems to solve exactly what I need but I don’t understand how user naschilling (second post) came up with the Gaussian matrix values. Context: I’m working on a music waveform generator (borrowing from SoundCloud’s design) that maps the amplitude of the song at time t to a corresponding bar height. Unfortunately there’s a lot of noise, and it looks particularly ugly when the program maps a tiny amplitude which results in a sudden decrease in height. I basically want to smooth out the bar heights so they aren’t so varied. The language I'm using is Javascript. EDIT: Sorry, let me be more specific about "smoothing out" the values. According to the thread linked above, a user took an array [10.00, 13.00, 7.00, 11.00, 12.00, 9.00, 6.00, 5.00] and used a Gaussian function to map it to [ 8.35, 9.35, 8.59, 8.98, 9.63, 7.94, 5.78, 7.32] Notice how the numbers are much closer to each other. EDIT 2: It worked! Thanks to user Awal Garg's algorithm, here are the results: No smoothing Some smoothing Maximum smoothing EDIT 3: Here's my final code in JS. I tweaked it so that the first and last elements of the array were able to find its neighbors by wrapping around the array, rather than calling itself. var array = [10, 13, 7, 11, 12, 9, 6, 5]; function smooth(values, alpha) { var weighted = average(values) * alpha; var smoothed = []; for (var i in values) { var curr = values[i]; var prev = smoothed[i - 1] || values[values.length - 1]; var next = curr || values[0]; var improved = Number(this.average([weighted, prev, curr, next]).toFixed(2)); smoothed.push(improved); } return smoothed; } function average(data) { var sum = data.reduce(function(sum, value) { return sum + value; }, 0); var avg = sum / data.length; return avg; } smooth(array, 0.85);
Interesting question! The algorithm to smooth out the values obviously could vary a lot, but here is my take: "use strict"; var array = [10, 13, 7, 11, 12, 9, 6, 5]; function avg (v) { return v.reduce((a,b) => a+b, 0)/v.length; } function smoothOut (vector, variance) { var t_avg = avg(vector)*variance; var ret = Array(vector.length); for (var i = 0; i < vector.length; i++) { (function () { var prev = i>0 ? ret[i-1] : vector[i]; var next = i<vector.length ? vector[i] : vector[i-1]; ret[i] = avg([t_avg, avg([prev, vector[i], next])]); })(); } return ret; } function display (x, y) { console.clear(); console.assert(x.length === y.length); x.forEach((el, i) => console.log(`${el}\t\t${y[i]}`)); } display(array, smoothOut(array, 0.85)); NOTE: It uses some ES6 features like fat-arrow functions and template strings. Firefox 35+ and Chrome 45+ should work fine. Please use the babel repl otherwise. My method basically computes the average of all the elements in the array in advance, and uses that as a major factor to compute the new value along with the current element value, the one prior to it, and the one after it. I am also using the prior value as the one newly computed and not the one from the original array. Feel free to experiment and modify according to your needs. You can also pass in a "variance" parameter to control the difference between the elements. Lowering it will bring the elements much closer to each other since it decreases the value of the average. A slight variation to loosen out the smoothing would be this: "use strict"; var array = [10, 13, 7, 11, 12, 9, 6, 5]; function avg (v) { return v.reduce((a,b) => a+b, 0)/v.length; } function smoothOut (vector, variance) { var t_avg = avg(vector)*variance; var ret = Array(vector.length); for (var i = 0; i < vector.length; i++) { (function () { var prev = i>0 ? ret[i-1] : vector[i]; var next = i<vector.length ? vector[i] : vector[i-1]; ret[i] = avg([t_avg, prev, vector[i], next]); })(); } return ret; } function display (x, y) { console.clear(); console.assert(x.length === y.length); x.forEach((el, i) => console.log(`${el}\t\t${y[i]}`)); } display(array, smoothOut(array, 0.85)); which doesn't take the averaged value as a major factor. Feel free to experiment, hope that helps!
The technique you describe sounds like a 1D version of a Gaussian blur. Multiply the values of the 1D Gaussian array times the given window within the array and sum the result. For example Assuming a Gaussian array {.242, .399, .242} To calculate the new value at position n of the input array - multiply the values at n-1, n, and n+1 of the input array by those in (1) and sum the result. eg for [3, 5, 0, 8, 4, 2, 6], n = 1: n1 = 0.242 * 3 + 0.399 * 5 + 0.242 * 0 = 2.721 You can alter the variance of the Gaussian to increase or reduce the affect of the blur.
i stumbled upon this post having the same problem with trying to achieve smooth circular waves from fft averages. i've tried normalizing, smoothing and wildest math to spread the dynamic of an array of averages between 0 and 1. it is of course possible but the sharp increases in averaged values remain a bother that basically makes these values unfeasable for direct display. instead i use the fft average to increase amplitude, frequency and wavelength of a separately structured clean sine. imagine a sine curve across the screen that moves right to left at a given speed(frequency) times the current average and has an amplitude of current average times whatever will then be mapped to 0,1 in order to eventually determine 'the wave's' z. the function for calculating size, color, shift of elements or whatever visualizes 'the wave' will have to be based on distance from center and some array that holds values for each distance, e.g. a certain number of average values. that very same array can instead be fed with values from a sine - that is influenced by the fft averages - which themselves thus need no smoothing and can remain unaltered. the effect is pleasingly clean sine waves appearing to be driven by the 'energy' of the sound. like this - where 'rings' is an array that a distance function uses to read 'z' values of 'the wave's x,y positions. const wave = { y: height / 2, length: 0.02, amplitude: 30, frequency: 0.5 } //var increment = wave.frequency; var increment = 0; function sinewave(length,amplitude,frequency) { ctx.strokeStyle = 'red'; ctx.beginPath(); ctx.moveTo(0, height / 2); for (let i = 0; i < width; i+=cellSize) { //ctx.lineTo(i, wave.y + Math.sin(i * wave.length + increment) * wave.amplitude) ctx.lineTo(i, wave.y + Math.sin(i * length + increment) * amplitude); rings.push( map( Math.sin(i * length + increment) * amplitude,0,20,0.1,1) ); rings.shift(); } ctx.stroke(); increment += frequency; } the function is called each frame (from draw) with the current average fft value driving the sine function like this - assuming that value is mapped to 0,1: sinewave(0.006,averg*20,averg*0.3) allowing fluctuating values to determine wavelength or frequency can have some visually appealing effect. however, the movement of 'the wave' will never seem natural. i've accomplished a near enough result in my case. for making the sine appear to be driven by each 'beat' you'd need beat detection to determine the exact tempo of 'the sound' that 'the wave' is supposed to visualize. continuous averaging of distance between larger peaks in the lower range of fft spectrum might work there with setting a semi fixed frequency - with edm... i know, the question was about smoothing array values. forgive me for changing the subject. i just thought that the objective 'sound wave' is an interesting one that could be achieved differently. and just so this is complete here's a bit that simply draws circles for each fft and assign colour according to volume. with linewidths relative to total radius and sum of volumes this is quite nice: //col generator function getCol(n,m,f){ var a = (PIx5*n)/(3*m) + PIdiv2; var r = map(sin(a),-1,1,0,255); var g = map(sin(a - PIx2/3),-1,1,0,255); var b = map(sin(a - PIx4/3),-1,1,0,255); return ("rgba(" + r + "," + g + "," + b + "," + f + ")"); } //draw circles for each fft with linewidth and colour relative to value function drawCircles(arr){ var nC = 20; //number of elem from array we want to use var cAv = 0; var cAvsum = 0; //get the sum of all values so we can map a single value with regard to this for(var i = 0; i< nC; i++){ cAvsum += arr[i]; } cAv = cAvsum/nC; var lastwidth = 0; //draw a circle for each elem from array //compute linewith a fraction of width relative to value of elem vs. sum of elems for(var i = 0; i< nC; i++){ ctx.beginPath(); var radius = lastwidth;//map(arr[i]*2,0,255,0,i*300); //use a small col generator to assign col - map value to spectrum ctx.strokeStyle = getCol(map(arr[i],0,255,0,1280),1280,0.05); //map elem value as fraction of elem sum to linewidth/total width of outer circle ctx.lineWidth = map(arr[i],0,cAvsum,0,width); //draw ctx.arc(centerX, centerY, radius, 0, Math.PI*2, false); ctx.stroke(); //add current radius and linewidth to lastwidth var lastwidth = radius + ctx.lineWidth/2; } } codepen here: https://codepen.io/sumoclub/full/QWBwzaZ always happy about suggestions.