Змея в Javascript

2
L292092 2020-07-11 08:57.

Я чувствую, что что-то не так со стилем моего кода, но я не могу точно определить, что с ним не так. Может быть, способ, которым я изобразил змею, немного запутан?

const HEIGHT = 20;
const WIDTH = 20;

const snake = function() {
    const snakeCoordinates = [[0, 0], [1, 0]];
    const moveDirections = [[-1, 0], [-1, 0]];

    function shiftDirections() {
        for (let i = snakeCoordinates.length; i > 0; i--) 
            moveDirections[i] = moveDirections[i - 1];
    }

    return {
        hasSnake: function(row, col) {
            return snakeCoordinates.reduce((accum, [snakeRow, snakeCol]) => accum || (row === snakeRow && col === snakeCol), false);
        },

        expandSnake: function() {       
            const head = snakeCoordinates[0];
            const headDirection = moveDirections[0];

            snakeCoordinates.unshift(wrapAround([head[0] + headDirection[0], head[1] + headDirection[1]])); 
            moveDirections.unshift(headDirection);
        },

        changeDirection: function(x, y) {
            const isAboutTurn = x === -moveDirections[0][0] || y === -moveDirections[0][1];

            if (!isAboutTurn)
                moveDirections[0] = [x, y];
        },

        getSnakeHead: function() {
            return snakeCoordinates[0];
        },

        isSnakeDead: function() {
            const tail = snakeCoordinates.slice(1);
            const head = snakeCoordinates[0];

            return tail.reduce((accum, [row, col]) => accum || (row === head[0] && col === head[1]), false);
        },

        moveSnake: function() {         
            for (let i = 0; i < snakeCoordinates.length; i++) {
                snakeCoordinates[i][0] += moveDirections[i][0];
                snakeCoordinates[i][1] += moveDirections[i][1];

                snakeCoordinates[i] = wrapAround(snakeCoordinates[i]);
            }                   

            shiftDirections();  
        }
    };
}();

const goal = function() {
    var goal = [HEIGHT - 1, WIDTH - 1];

    return {
        newGoal: function() {
            goal = [rng(0, HEIGHT), rng(0, WIDTH)];
        },

        isGoal: function(row, col) {
            return goal[0] === row && goal[1] === col;
        }
    };
}();

initialiseGame();

function initialiseGame() {
    initialiseGridInDOM();
    addEventListeners();

    runGame();
}

function addEventListeners() {
    document.addEventListener("keydown", dealWithKeyPress);
}

// Map key press onto action
function dealWithKeyPress(keyPress) {
    const leftArrow = 37;
    const upArrow = 38;
    const rightArrow = 39;
    const downArrow = 40;

    switch (keyPress.keyCode) {
        case upArrow:
            snake.changeDirection(-1, 0);
            break;
        case leftArrow:
            snake.changeDirection(0, -1);
            break;
        case rightArrow:
            snake.changeDirection(0, 1);
            break;
        case downArrow:
            snake.changeDirection(1, 0);
            break;
    }
}

async function runGame() {
    var running = true;

    while (running) {       
        snake.moveSnake();  
        checkForSnakeDeath(gameOver);
        checkForGoalCapture();
        updateDOM();
        await delay();
    }

    function gameOver() {
        alert("Game over!");
        running = false;
    }
}

function checkForSnakeDeath(gameOver) {
    if (snake.isSnakeDead())
        gameOver();
}

function checkForGoalCapture() {
    const head = snake.getSnakeHead();

    if (goal.isGoal(head[0], head[1])) {
        snake.expandSnake();
        goal.newGoal();
    }
}

function delay() {
    return new Promise(resolve => {
        setTimeout(resolve, 60);
    });
}

function updateDOM() {
    for (let row = 0; row < HEIGHT; row++) 
        for (let col = 0; col < WIDTH; col++) 
            if (snake.hasSnake(row, col)) 
                colorSnakeInDOM(row, col);
            else if (goal.isGoal(row, col))
                colorGoalInDOM(row, col);
            else
                colorWallInDOM(row, col);
}

function colorSnakeInDOM(row, col) {
    const SNAKE_COLOR = "#00BFFF";

    colorTileInDOM(SNAKE_COLOR, row, col);
}

function colorWallInDOM(row, col) {
    const WALL_COLOR = selectBackgroundColor(row, col);

    colorTileInDOM(WALL_COLOR, row, col);
}

function colorGoalInDOM(row, col) {
    const GOAL_COLOR = "yellow";
        
    colorTileInDOM(GOAL_COLOR, row, col);
}

function colorTileInDOM(color, row, col) {
    const tileDOM = getTileInDOM(row, col);

    tileDOM.style.backgroundColor = color;
}

function getTileInDOM(row, col) {
    const gridDOM = document.querySelector("#grid");
    const rowDOM = gridDOM.rows[row];
    const tileDOM = rowDOM.cells[col];

    return tileDOM;
}

// Dynamically generate HTML for a plain grid
function initialiseGridInDOM() {
    const gridDOM = document.querySelector("#grid");

    for (let row = 0; row < HEIGHT; row++) {
        let newRow = createEmptyRowInDOM(row);
        gridDOM.append(newRow);
    }

    updateDOM();
}   

function createEmptyRowInDOM(row) {
    const newRow = document.createElement("tr");
    newRow.className = "row";

    for (let col = 0; col < WIDTH; col++)  {
        let newTile = createEmptyTileInDOM(row, col);
        newRow.append(newTile);
    }

    return newRow;
}

function createEmptyTileInDOM(row, col) {
    const newTile = document.createElement("td");

    newTile.className = "tile";

    return newTile;
}

function selectBackgroundColor(row, col) {
    const BACKGROUND_COLOR1 = "#3dfc03";
    const BACKGROUND_COLOR2 = "#03fc03";

    if ((row + col) % 2 === 0)
        return BACKGROUND_COLOR1;
    else
        return BACKGROUND_COLOR2;
}

function wrapAround([row, col]) {
    if (row === HEIGHT) 
        row = 0;
    if (row < 0)
        row = HEIGHT - 1;    
    if (col === WIDTH)
        col = 0;
    if (col < 0)
        col = WIDTH - 1;

    return [row, col];
}

// Generates a random number whose value lies between lower and upper
function rng(lower, upper) {
    return Math.floor(Math.random() * (upper - lower)) + lower;
}

```

1 answers

3
Hugeen 2020-07-11 22:21.

Во-первых, будьте снисходительны, английский не мой родной язык :).

Вот мой вариант твоего сценария. Прокомментирую внесенные мной изменения.

Я разделил ваш сценарий на главы (которые могут привести к разделению файлов, если хотите):

  • Логика игры (змейка и цель)
  • Ход игры (начало, цикл, игра окончена)
  • Управление
  • Рендеринг

Я произвел некоторое выравнивание переменных и использовал сокращенное определение метода ES6 (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Method_definitions)


ЛОГИКА ИГРЫ

Для объекта змеи я заменил самовыполняющуюся функцию на именованную функцию для лучшей читаемости.

Я также сократил имена методов, потому что мы уже знаем, что находимся на объекте змеи.

Например, я изменил snake.expandSnake () на snake.expand ().

Сокращение онлайнеров было немного трудно читать, поэтому я изменил их на многострочные.

const HEIGHT = 20
const WIDTH  = 20



// GAME LOGIC

function createSnake () {

    const coordinates    = [[0, 0], [1, 0]]
    const moveDirections = [[-1, 0], [-1, 0]]

    function shiftDirections () {
        for (let i = coordinates.length; i > 0; i--) {
            moveDirections[i] = moveDirections[i - 1]
        }
    }

    return {

        hasSnake (row, col) {
            return coordinates.reduce(function (accum, [snakeRow, snakeCol]) {
                return accum || (row === snakeRow && col === snakeCol)
            }, false)
        },

        expand () {
            const head = coordinates[0]
            const headDirection = moveDirections[0]

            coordinates.unshift(
                wrapAround([
                    head[0] + headDirection[0],
                    head[1] + headDirection[1]
                ])
            )

            moveDirections.unshift(headDirection)
        },

        changeDirection (x, y) {
            const isAboutTurn = (
                x === -moveDirections[0][0] ||
                y === -moveDirections[0][1]
            )

            if (!isAboutTurn) {
                moveDirections[0] = [x, y]
            }
        },

        getHead () {
            return coordinates[0]
        },

        isDead () {
            const tail = coordinates.slice(1)
            const head = coordinates[0]

            return tail.reduce(function (accum, [row, col]) {
                return accum || (row === head[0] && col === head[1])
            }, false)
        },

        move () {
            for (let i = 0; i < coordinates.length; i++) {
                coordinates[i][0] += moveDirections[i][0]
                coordinates[i][1] += moveDirections[i][1]

                coordinates[i] = wrapAround(coordinates[i])
            }                   

            shiftDirections()
        }

    }

}



function createGoal () {

    let coords = [HEIGHT - 1, WIDTH - 1]

    return {
        newGoal: function () {
            coords = [randomIntBetween(0, HEIGHT), randomIntBetween(0, WIDTH)]
        },

        isGoal: function (row, col) {
            return coords[0] === row && coords[1] === col
        }
    }

}

ИГРОВОЙ ПОТОК

Вы можете видеть, что в этой главе созданы объекты змея и цель.

Я переименовал checkForSnakeDeath и checkForGoalCapture в handleSnakeDeath и handleGoal, потому что то, что вы делаете в этих функциях, - это больше, чем простая проверка. На мой взгляд, проверка должна просто возвращать истину или ложь, или, возможно, объект ошибки, но не выполнять действия.

// GAME FLOW

let snake = createSnake()
let goal  = createGoal()



initialiseGame()

function initialiseGame () {
    initialiseDOMGrid()
    addEventListeners()

    runGame()
}



function addEventListeners () {
    document.addEventListener('keydown', moveSnakeFromInput)
}


async function runGame () {

    var running = true

    while (running) {
        snake.move()
        handleSnakeDeath(gameOver)
        handleGoal()

        paintGrid()

        await delay()
    }

    function gameOver () {
        alert('Game over!')
        running = false
    }

}


function handleSnakeDeath (callback) {
    if (snake.isDead()) {
        callback()
    }
}


function handleGoal () {
    const head = snake.getHead()

    if (goal.isGoal(head[0], head[1])) {
        snake.expand()
        goal.newGoal()
    }
}


function delay () {
    return new Promise(resolve => {
        setTimeout(resolve, 60)
    })
}

УПРАВЛЕНИЕ

Для элементов управления я изменил «переключатель» на сопоставление клавиш и методы объектов для лучшей читабельности и модульности (теперь вы можете выполнять move.left () программно, если хотите).

// CONTROLS

const moves = {
    left () {
        snake.changeDirection(-1, 0)
    },
    up () {
        snake.changeDirection(0, -1)
    },
    right () {
        snake.changeDirection(0, 1)
    },
    down () {
        snake.changeDirection(1, 0)
    }
}

const keysMap = {
    37: 'left',
    38: 'up',
    39: 'right',
    40: 'down'
}


function moveSnakeFromInput (keyPress) {
    if (keyPress in keysMap) {
        moves[keysMap[keyPress]]()
    }
}

ОФОРМЛЕНИЕ

Для рендеринга я объединил цветовые переменные для упрощения изменений. Я разделил updateDOM (который теперь называется paintGrid) на два метода: forEachCell и paintCell.

// RENDERING


const SNAKE_COLOR  = '#00BFFF'
const GOAL_COLOR   = 'yellow'
const BACKGROUND_1 = '#3dfc03'
const BACKGROUND_2 = '#03fc03'


function paintGrid () {
    forEachCell(paintCell)
}


function forEachCell (iterator) {
    for (let row = 0; row < HEIGHT; row++) {
        for (let col = 0; col < WIDTH; col++) {
            iterator(row, col)
        }
    }
}


function paintCell (row, col) {
    if (snake.hasSnake(row, col)) {
        paintSnake(row, col)
    } else if (goal.isGoal(row, col)) {
        paintGoal(row, col)
    } else {
        paintWall(row, col)
    }
}


function paintSnake (row, col) {
    paintTile(SNAKE_COLOR, row, col)
}


function paintWall (row, col) {
    const WALL_COLOR = getBackgroundColor(row, col)

    paintTile(WALL_COLOR, row, col)
}


function paintGoal (row, col) {
    paintTile(GOAL_COLOR, row, col)
}


function paintTile (color, row, col) {
    const tileDOM = getTile(row, col)

    tileDOM.style.backgroundColor = color
}


function getTile (row, col) {
    const gridDOM = document.querySelector('#grid')
    const rowDOM = gridDOM.rows[row]
    const tileDOM = rowDOM.cells[col]

    return tileDOM
}


function initialiseDOMGrid () {
    const gridDOM = document.querySelector('#grid')

    for (let row = 0; row < HEIGHT; row++) {
        let newRow = createDOMRow(row)
        gridDOM.append(newRow)
    }

    paintGrid()
}   


function createDOMRow () {
    const newRow = document.createElement('tr')
    newRow.className = 'row'

    for (let col = 0; col < WIDTH; col++)  {
        let newTile = createDOMTile()
        newRow.append(newTile)
    }

    return newRow
}


function createDOMTile () {
    const newTile = document.createElement('td')
    newTile.className = 'tile'

    return newTile
}


function getBackgroundColor (row, col) {
    return (row + col) % 2 === 0 ? BACKGROUND_1 : BACKGROUND_2
}

UTILS

Просто некоторые незначительные изменения читабельности здесь

// UTILS


function wrapAround ([row, col]) {
    if (row === HEIGHT) {
        row = 0
    }

    if (row < 0) {
        row = HEIGHT - 1
    }

    if (col === WIDTH) {
        col = 0
    }

    if (col < 0) {
        col = WIDTH - 1
    }

    return [row, col]
}


function randomIntBetween (lower, upper) {
    return Math.floor(Math.random() * (upper - lower)) + lower
}

Очевидно, это только мое мнение, так что просто выберите то, что вам нравится в моих модификациях :-)

Related questions

MORE COOL STUFF

Яна Дуггар: все, что она говорила о любви и ее 5-летнем окне, чтобы найти «того самого»

Яна Дуггар: все, что она говорила о любви и ее 5-летнем окне, чтобы найти «того самого»

Яна Дуггар открыто говорила о своих поисках любви. Вот все, что она сказала на эту тему и о своем пятилетнем окне для замужества.

Сэм Хьюэн точно знает, что он возьмет из съемочной площадки «Чужестранки»: «Я чувствую себя Джейми, когда надеваю их»

Сэм Хьюэн точно знает, что он возьмет из съемочной площадки «Чужестранки»: «Я чувствую себя Джейми, когда надеваю их»

Звезда «Чужестранки» Сэм Хьюэн недавно рассказал, что он возьмет со съемочной площадки, чтобы вспомнить Джейми Фрейзера, роль, которая сделала его звездой.

Ранняя песня Beatles, которая вошла в число «самых захватывающих выступлений» группы, по мнению инсайдера Fab Four

Ранняя песня Beatles, которая вошла в число «самых захватывающих выступлений» группы, по мнению инсайдера Fab Four

«She Loves You» была написана за час, записана за день и стала песней The Beatles с одним из лучших исполнений в их карьере.

Долли Партон помогла своей «инвалидной» бабушке, когда ни один из других внуков не стал бы — она тоже разыгрывала ее

Долли Партон помогла своей «инвалидной» бабушке, когда ни один из других внуков не стал бы — она тоже разыгрывала ее

У Долли Партон и ее бабушки Бесси были особые отношения. Бабушка Партон была очень больна, но это не мешало Долли разыгрывать ее.

Какая древесина самая твёрдая в мире?

Какая древесина самая твёрдая в мире?

Вы можете подумать, что дуб или гикори — прочные породы дерева, но когда речь идет о самой твердой древесине в мире, они даже близко не стоят.

10 самых страшных морских существ, скрывающихся в глубинах океана

10 самых страшных морских существ, скрывающихся в глубинах океана

Океан полон красоты, но он также скрывает одних из самых страшных морских существ на планете. Многие из этих животных скрываются глубоко под поверхностью океана, в тёмном, полном давления мире морских глубин.

10 лучших чар для мечей в Minecraft

10 лучших чар для мечей в Minecraft

Если вы сражаетесь с враждебными мобами или готовитесь к PvP-боям, знание лучших чар для мечей в Minecraft может дать вам серьёзное преимущество. Зачарование мечей позволяет наносить больше урона, увеличивать количество выпадающих мобов и продлевать срок службы меча.

13 социалистических стран, которые сильно отличаются друг от друга

13 социалистических стран, которые сильно отличаются друг от друга

Когда говорят о социалистических странах, часто представляют себе полный государственный контроль и отсутствие частной собственности. Но на практике социалистические экономики сильно различаются.

Резюме «Медведя»: Маркус — самый добрый, мягкий и искренний человек в Чикаго?

Резюме «Медведя»: Маркус — самый добрый, мягкий и искренний человек в Чикаго?

В «Детях» появится громкая камео и несколько разрушительных новостей

За 9800 долларов вы бы упаковали их в эту семиместную Toyota RAV4 2008 года выпуска?

За 9800 долларов вы бы упаковали их в эту семиместную Toyota RAV4 2008 года выпуска?

Утверждается, что этот RAV4 находится в отличном состоянии и готов к игре в музыкальные стулья.

Фургон доставки Amazon загорелся в результате огненного взрыва во время летней жары в Хьюстоне

Фургон доставки Amazon загорелся в результате огненного взрыва во время летней жары в Хьюстоне

На кадрах, снятых с дрона, видно, как пожарные изо всех сил пытаются потушить пламя.

«Шоу Черной Пантеры» от Marvel станет самым важным анимационным шоу за всю историю

«Шоу Черной Пантеры» от Marvel станет самым важным анимационным шоу за всю историю

«Глаза Ваканды» напрямую связаны с MCU, а также обновления о «Людях Икс '97», «Что, если...», «Сорвиголова» и многом другом.

История знакомств Авы Гарднер: взгляд назад на браки и романы голливудской иконы

История знакомств Авы Гарднер: взгляд назад на браки и романы голливудской иконы

Ава Гарднер была замужем за Микки Руни и Фрэнком Синтрой, и встречалась с несколькими другими голливудскими звездами. Вот взгляд назад на историю знакомств Авы Гарднер.

Кто жена Джона Клиза? Все о Дженнифер Уэйд

Кто жена Джона Клиза? Все о Дженнифер Уэйд

Джон Клиз женился на Дженнифер Уэйд в 2012 году. Вот все, что нужно знать о жене Джона Клиза, Дженнифер Уэйд.

Кто муж Эллисон Феликс? Все о Кеннете Фергюсоне

Кто муж Эллисон Феликс? Все о Кеннете Фергюсоне

Олимпийская чемпионка Эллисон Феликс вышла замуж за своего мужа Кеннета Фергюсона в 2018 году. Вот все, что нужно знать о муже Эллисон Феликс, Кеннете Фергюсоне.

Кто муж Мены Сувари? Все о Майкле Хоупе

Кто муж Мены Сувари? Все о Майкле Хоупе

Мена Сувари замужем за своим мужем Майклом Хоупом с 2018 года. Вот все, что нужно знать о муже Мены Сувари.

местоимения очень страшные

Это поражает меня Внезапно, как это часто бывает, Это снова и снова на полпути <Нет, не то, что в Тихом океане, ты темно-бордовый, то, что на карнавале> Каждый громкий заголовок…..

3 проверенных способа улучшить свои навыки письма

Преимущества письма и стратегии, которые помогут вам в этом

3 проверенных способа улучшить свои навыки письма

В 2022 году, с выпуском GPT-3, навыки письма, кажется, стали менее важными. С помощью ИИ люди с плохим письмом также могут генерировать качественный персонал.

Медленная эрозия: раскрытие того, как люди угасают в своей карьере

Медленная эрозия: раскрытие того, как люди угасают в своей карьере

В современном быстро меняющемся и конкурентном мире карьера играет важную роль в личностном росте, финансовой стабильности и общей удовлетворенности жизнью. Однако многие люди оказываются в безжалостном цикле застоя и неудовлетворенности, постепенно угасая в своей профессиональной жизни.

Что делает подкаст влиятельным?

Что делает подкаст влиятельным?

На прошлой неделе я заметил пресс-релиз, разосланный через PressGazette (новостной веб-сайт британских СМИ). В статье сообщалось, что Acast, скандинавская компания по размещению и рекламе подкастов, возглавит консорциум «вероятно, самых влиятельных» издателей подкастинга.

Language