2
\$\begingroup\$

I feel like there's something wrong with the way my code is styled but I can't quite place what's wrong with it. Perhaps the way I've represented the snake is a little convoluted?

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;
}

```
\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

First, please be indulgent, english is not my native langage :).

Here is my take on your script. I will comment on the modifications I have made.

I have separated your script into chapters (that can lead to separated files if you want) :

  • Game logic (snake and goal)
  • Game flow (start, loop, game over)
  • Controls
  • Rendering

I have made some variable alignment and used ES6 shortened method definition (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Method_definitions)


GAME LOGIC

For the snake object I have changed the self executing function to a named function for a better readability.

I have also shortened the method names because we already know we are on the snake object.

For example, I changed snake.expandSnake() to snake.expand().

The reduce onliners were a bit hard to read, so I changed them to multi line.

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
        }
    }

}

GAME FLOW

You can see the objects snake and goal are now created in this chapter.

I have renamed checkForSnakeDeath and checkForGoalCapture into handleSnakeDeath and handleGoal, because what you are doing in thoses functions is more than a simple check. In my opinion, a check should just return true or false, or maybe an error object but not doing actions.

// 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)
    })
}

CONTROLS

For the controls I have changed the "switch" to key mapping and object methods for better readability and modularity (you can now do moves.left() programmatically if you want).

// 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]]()
    }
}

RENDERING

For the rendering I have put the color variables together for easier changes. I have split updateDOM (which is now called paintGrid) into two methods : forEachCell and 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

Just some minor readability modifications here

// 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
}

Obviously this is just my opinion, so just pick what you like in my modifications :-)

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.