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)