так что я работаю над этой игрой со змеями, и я в основном пытаюсь предотвратить появление еды на вершине змеиного хвоста. Мои настройки переменных:
let headX = 10; //snake starting position
let headY = 10;
let appleX = 5; //food starting position
let appleY = 5;
Это функция, которая проверяет столкновение головы и еды.
function checkAppleCollision() {
if (appleX === headX && appleY === headY) {
generateApplePosition();
tailLength++;
score++;
}
}
А это функция, которая рандомизирует положение яблока после столкновения, а также возвращает ошибку «слишком много рекурсии» после пары столкновений:
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;
}
Пожалуйста, помогите, я понятия не имею, что здесь делать. Все остальное работает как задумано.
Я просто подумал, что это может быть путь, конечно, это, вероятно, не оптимально. У меня нет предыдущего опыта работы с проектами такого типа, поэтому не могли бы вы посоветовать, как лучше ввести функцию?
1. Задача вообще не требует рекурсии. Вы мог делаете это, но это немного лишнее. 2. Код не работает для рекурсии. Суть рекурсии состоит в том, чтобы итеративно создавать более простые проблемы, пока не дойдете до той, которую можно решить (базовый случай), а затем итеративно решать упрощенные задачи в обратном порядке. Здесь нет базового случая. Нет ничего, чтобы остановить рекурсию. while (collisionDetect === true)
гарантирует, что если условие истинно, вы получите ошибку всегда. Поскольку функция вызывается бесконечное количество раз, пока рекурсивный вызов также не попадет в while
и т. д.
Ваша рекурсия зависит от переменной, которая никогда не обновляется - collisionDetect
в вызываемой функции полностью отличается от той, что в вызывающей функции.
Что вы пытались решить проблему? Где ты застрял?
Как уже говорили другие, это не обязательно должно быть рекурсивным, и вы также должны учитывать (хотя и маловероятную) возможность, когда больше нет плиток для появления, что приведет к бесконечному циклу.
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);
}
}
if (tilesLeft > 0) {
смысла нет. Если плиток не осталось, это часть логики isGameOver()
. Функция, которая генерирует позицию яблока, не должна беспокоиться о ненужных вещах.
Согласен, однако, поскольку никакая остальная игровая логика не была представлена, в этом контексте вполне обоснованно следует избегать бесконечного цикла.
Использование рекурсий или do while
— плохая идея (объясню позже)
тем временем вы можете упростить свою логику, создав:
samePos()
и collides()
createApple()
, которая вернет себя, если случайно сгенерированные позиции x, y будут заняты телом змеи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.info(`position ${randPos.x} ${randPos.y} is occupied by snake`);
return createApple(); // Try another position.
}
// Finally a free spot!
apple.pos = randPos;
console.info(`Apple to free position: ${apple.pos.x} ${apple.pos.y}`);
}
createApple();
Run this demo multiple times
Бесполезные случайные догадки!
Как вы можете видеть из приведенного выше примера, если вы запускаете его несколько раз, очень часто случайно сгенерированное число совпадает с ранее сгенерированным:
...
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 <<<
...
следовательно, по мере того, как ваша змейка увеличивается в размерах, рекурсия может сойти с ума — до абсурда, повторяясь слишком много раз, повторяясь и терпя неудачу в одних и тех же позициях xy, пока, наконец, не наткнетесь на редкое свободное место...
Это действительно плохой дизайн.
Одним из решений было бы отслеживать уже использованные рандомизированные позиции внутри массива, но это подразумевает излишнее прохождение такого массива.
Лучшее решение на самом деле будет рассматривать игру не как 2D игра, а как 1D игра:
Рассмотрим эту 2D-карту размером 4x3 как индексы:
0 1 2 3
4 5 6 7
8 9 10 11
теперь давайте поместим змею на эту карту:
0 ⬛ 2 3
4 ⬛ ⬛ 7
8 9 ⬛ 11
вот линейная карта со Змеей в виде одномерного списка:
[ 0 ⬛ 2 3 4 ⬛ ⬛ 7 8 9 ⬛ 11 ]
поэтому вместо использования массива объектов {x:n, y:n}
для положения тела змеи все, что вам нужно, это:
[1, 5, 6, 10] // Snake body as indexes
Теперь, когда вы знаете все индексы, в которых вам не разрешено размещать яблоко, все, что вам нужно сделать при создании нового яблока, это:
delete
эти индексы из массива индексов, чтобы получить Массив индексов свободных мест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.info("New apple position: %o", applePos);
};
createApple();
Run this demo multiple times
Имея этот индекс свободного места, просто нарисуйте свое яблоко в координатах XY, используя эту простую формулу.
Х = индекс % ширина карты
Y = пол (индекс / ширина карты)
Вау, какой подробный и информативный ответ. Буду внимательно изучать, спасибо за информацию! Кажется, теперь игра работает так, как задумано, но я воспользуюсь вашим советом и попытаюсь реализовать то, что вы здесь указали.
@EddieBenzon Спасибо. Безусловно! В качестве хорошего домашнего задания вы можете попробовать однажды переделать ту же игру в линейное 1D (используя индексы). Единственной более сложной задачей будет преобразование ключевых направлений в соответствующий индекс N, S, E, W (с некоторой базовой математикой). Остальное довольно просто. Даже если вы создаете свою игру, используя элементы DIV в сетке, проще раскрасить определенные элементы DIV, используя эти индексы. Попробуйте.
Почему это вообще должно быть рекурсивным?