7
\$\begingroup\$

Coming from a Java background, this is my first JavaScript-only game. I'm looking for some advice on what to improve.

Also, I'm having trouble when it comes to program design. Sometimes I don't know if I should write class-like code or just plain functions. How does one go about deciding on this?

Code of the snake:

if (!window.requestAnimationFrame) {
    window.requestAnimationFrame = (function() {
        return window.webkitRequestAnimationFrame ||
            window.mozRequestAnimationFrame ||
            window.oRequestAnimationFrame ||
            window.msRequestAnimationFrame ||
            function(callback) {
                //gameloop fallbacks to 60fps
                window.setTimeout(callback, 1000 / 60);
        };
    })();
}



var canvas,
    context,
    Game,
    Snake,
    Food,
    Direction;



canvas = document.createElement('canvas');
canvas.width = 800;
canvas.height = 600;
context = canvas.getContext('2d');
document.body.appendChild(canvas);



Game = (function() {
    var ready = false;

    var blockSize = 20;

    var mapFillStyle = '#0D0D0D';

    var mapStrokeStyle = '#002608';

    var score = 0;

    function start() {
        ready = true;
    };

    function stop() {
        ready = false;
    };

    function getBlockSize() {
        return blockSize;
    };

    function applyStyle(ctx, x, y) {
        ctx.fillRect(x * blockSize, y * blockSize, blockSize, blockSize);
        ctx.lineWidth = .5;
        ctx.strokeRect(x * blockSize, y * blockSize, blockSize, blockSize);
    };

    function isReady() {
        return ready;
    };

    function clearMap() {
        context.fillStyle = mapFillStyle;
        context.strokeStyle = mapStrokeStyle;
        for (var x = canvas.width - blockSize; x >= 0; x--) {
            for (var y = canvas.height - blockSize; y >= 0; y--) {
                applyStyle(context, x, y);
            };
        };
    };

    function showStartText() {
        context.font = "bold 35px sans-serif";
        context.fillStyle = '#FFFFFF';
        context.fillText("Press space bar to start!", canvas.width / 4, canvas.height / 2);
    };

    function showScore() {
        context.font = "bold 18px sans-serif";
        context.fillStyle = 'rgba(255, 255, 255, 0.3)';
        context.fillText("Score: " + score, canvas.width - 120, canvas.height - 20);
    }
    return {
        // initial FPS/Speed of the game
        FPS: 50,

        start: start,

        stop: stop,

        blockSize: getBlockSize,

        isReady: isReady,

        clearMap: clearMap,

        showStartText: showStartText,

        applyStyle: applyStyle,

        showScore: showScore,

        addScore: function() {
            score++;
        },

        resetScore: function() {
            score = 0;
        }
    };
}());



;
Direction = (function() {
    return {
        // keycode mappings for the arrow keys
        LEFT: 37,
        UP: 38,
        RIGHT: 39,
        DOWN: 40,
    };
}());



Food = (function() {

    var foodFillStyle = '#FF6363';
    var foodStrokeStyle = '#FFA8A8';

    var x = null;
    var y = null;

    var consumed = true;

    function generateRandomCoordinates() {
        return {
            x: Math.floor(Math.random() * (canvas.width / Game.blockSize())),
            y: Math.floor(Math.random() * (canvas.height / Game.blockSize()))
        };
    };

    function isValidCoordinates(x, y) {
        var valid = true;
        Snake.forEachSnakeSegment(function(segment) {
            if (segment.x === x && segment.y === y) {
                valid = false;
                this.
                break;
            }
        });
        return valid;
    }

    function generateValidCoordinates() {
        var haventFoundAValidCoordinates = true;
        var coordinates;
        while (haventFoundAValidCoordinates) {
            coordinates = generateRandomCoordinates();
            if (isValidCoordinates(coordinates.x, coordinates.y)) {
                haventFoundAValidCoordinates = false;
                break;
            }
        }
        x = coordinates.x;
        y = coordinates.y;
    };

    function checkIfConsumed() {
        Snake.forEachSnakeSegment(function(segment) {
            var snakeX = segment.x;
            var snakeY = segment.y;
            if (x === snakeX && y === snakeY) {
                consumed = true;
                this.
                break;
            }
        });
    };

    function draw() {

        checkIfConsumed();

        if (consumed) {
            Game.addScore();
            generateValidCoordinates();
            consumed = false;
            Snake.grow();
        }

        context.fillStyle = foodFillStyle;
        context.strokeStyle = foodStrokeStyle;

        Game.applyStyle(context, x, y);
    };

    function init() {
        if (consumed) {
            generateValidCoordinates();
            consumed = false;
        }

        context.fillStyle = foodFillStyle;
        context.strokeStyle = foodStrokeStyle;

        Game.applyStyle(context, x, y);
    };


    function consumed() {
        return consumed;
    };

    return {
        consumed: consumed,
        draw: draw,
        init: init
    };
}());



;
Snake = (function() {

    //initial size of the snake
    var initialSnakeSize = 5;

    //a queue data struct for the body of the snake
    var segments = [];

    //RIGHT = initial direction of snake
    var direction = Direction.RIGHT;

    var newDirection = Direction.RIGHT;

    var snakeFillStyle = '#FFFFFF';

    var snakeStrokeStyle = '#D1D1D1';

    var shouldGrow = false;

    function head() {
        return segments[0];
    };

    function forEachSnakeSegment(action) {
        if (action && action.length == 1) {
            for (var i = segments.length - 1; i >= 0; i--) {
                action(segments[i]);
            };
        }
    }

    function fillSegment(segment) {
        context.fillStyle = snakeFillStyle;
        context.strokeStyle = snakeStrokeStyle;
        Game.applyStyle(context, segment.x, segment.y);
    };

    function getNextHeadBasedOnDirection() {
        var x = head().x;
        var y = head().y;
        switch (newDirection) {
            case Direction.RIGHT:
                if (direction !== Direction.LEFT) {
                    x++;
                    direction = newDirection;
                }
                break;
            case Direction.LEFT:
                if (direction != Direction.RIGHT) {
                    x--;
                    direction = newDirection;
                }
                break;
            case Direction.UP:
                if (direction !== Direction.DOWN) {
                    y--;
                    direction = newDirection;
                }
                break;
            case Direction.DOWN:
                if (direction !== Direction.UP) {
                    y++;
                    direction = newDirection;
                }
                break;
            default:
                break;
        };
        return {
            x: x,
            y: y
        };
    };

    function setDirection(keyCode) {
        switch (keyCode) {
            case Direction.RIGHT:
                if (direction !== Direction.LEFT)
                    newDirection = Direction.RIGHT;
                break;
            case Direction.LEFT:
                if (direction != Direction.RIGHT)
                    newDirection = Direction.LEFT;
                break;
            case Direction.UP:
                if (direction !== Direction.DOWN)
                    newDirection = Direction.UP;
                break;
            case Direction.DOWN:
                if (direction !== Direction.UP)
                    newDirection = Direction.DOWN;
                break;
            default:
                break;
        };
    };

    function init() {
        segments = [];
        direction = Direction.RIGHT;
        newDirection = Direction.RIGHT;
        for (var i = initialSnakeSize - 1; i >= 0; i--) {
            segments.push({
                x: i,
                y: 0
            });
        };
    };

    /*
     *  updates the values of snake such as direction, etc etc.
     *  according to user-input
     */

    function update() {
        var nextHead = getNextHeadBasedOnDirection();
        if (shouldGrow) {
            segments.unshift(nextHead);
            shouldGrow = false;
        } else {
            var tail = segments.pop();
            tail.x = nextHead.x;
            tail.y = nextHead.y;
            segments.unshift(tail);
        }

    };

    /*
     *  draws the snake using the updated values
     */

    function draw() {
        forEachSnakeSegment(fillSegment);
    };

    function collided() {
        var nextHead = getNextHeadBasedOnDirection();
        var collided = false;

        //self collision
        forEachSnakeSegment(function(segment) {
            if (nextHead.x === segment.x && nextHead.y === segment.y) {
                collided = true;
                this.
                break;
            }
        });

        // x-wall collision
        if (nextHead.x * Game.blockSize() > canvas.width - Game.blockSize() || nextHead.x < 0)
            collided = true;

        //y-wall collision
        if (nextHead.y * Game.blockSize() > canvas.height - Game.blockSize() || nextHead.y < 0)
            collided = true;

        return collided;
    };

    function getSegments() {
        return segments;
    }


    function grow() {
        shouldGrow = true;
    };

    return {
        update: update,
        draw: draw,
        init: init,
        setDirection: setDirection,
        collided: collided,
        segments: getSegments,
        grow: grow,
        forEachSnakeSegment: forEachSnakeSegment
    };

})();



var registerKeyListeners = function() {
    document.addEventListener('keydown', function(e) {
        if (32 === e.keyCode) {
            Game.start();
        }
        Snake.setDirection(e.keyCode);
    }, false);
};



var gameLoop = function() {
    setTimeout(function() {
        var requestId = requestAnimationFrame(gameLoop);
        if (Game.isReady()) {
            Game.clearMap();
            if (Snake.collided()) {
                Game.stop();
                Game.resetScore();
            } else {
                Snake.update();
                Snake.draw();
                Food.draw();
            }
        } else {
            Game.clearMap();
            Snake.init();
            Snake.draw();
            Food.init();
            Game.showStartText();
        }
        Game.showScore();

    }, 1000 / Game.FPS);
};

registerKeyListeners();
gameLoop();
\$\endgroup\$
  • \$\begingroup\$ Welcome to Code Review! This is a nice first question. \$\endgroup\$ – RubberDuck Jul 2 '14 at 12:28
  • 1
    \$\begingroup\$ Just a very minor improvement: Direction is in fact just a simple object so in this state you don't need to wrap it with a function call. But I guess you did this so you can easily add private stuff later? \$\endgroup\$ – kayahr Jul 2 '14 at 15:54
  • \$\begingroup\$ @kayahr : you are right about the Direction, i guess I'll have to treat it simple. thanks though! \$\endgroup\$ – acera Jul 8 '14 at 7:43
3
\$\begingroup\$

Nice job. I have only a few minor improvements to offer, mostly because I also came from the Java/C++/C# world and wrote a game to learn JavaScript, and I know what I did wrong.

Sometimes I don't know if I should write class-like code or just plain functions. How does one go about deciding on this?

The general rule of JavaScript is to not pollute the global namespace. Ideally, you only have one symbol in the global namespace (function SnakeGame would be an example, and all others would be inside it, created by var snakeGame = new SnakeGame();).

The exception would be when you are writing code for one particular page. In that case, you know exactly what is on the page, so there is no need to take the time to hide names. Most of these functions will be inline, but if you have to share a function, I'll name it something simple and understandable, like:

function checkAll()

If you end up reusing this code, I'll put it in a utilities object or something that can hold lots of little helper functions. Eventually, you end up with a little library, at which time, you should investigate those libraries already out there.

Also, JavaScript doesn't have "classes". You know this, of course, but the mistake I made, and I see you making, is to try to get JavaScript to act like Java/C#/C++. Instead, JavaScript has objects and functions. Instead of this:

return {
    update: update,
    draw: draw,
    init: init,
    setDirection: setDirection,
    collided: collided,
    segments: getSegments,
    grow: grow,
    forEachSnakeSegment: forEachSnakeSegment
};

I suggest learning how to use the prototype property of objects. The reason I don't like the above idea is that it makes you write the name of a function twice more, which could lead to syntax errors, and it isn't standard coding.

Speaking of syntax errors, JavaScript does not compile your code and find errors until you actually run the code (or, sometimes, not even then). It is a much more forgiving language then Java, sometimes too forgiving. It helps a bit if you include "use strict"; at the top of your code (or in your top level function, if you are protecting the global namespace).

The last bit of advice is that, while it is good to know how to create document elements within JavaScript, it is much more flexible to allow someone outside of the JavaScript create and size the element (perhaps in the html and css), and just pass the element into the SnakeGame constructor function. In this world, we often have multiple people working on the same page, one doing markup and the other doing JavaScript, so if you get into the habit of keeping those separate, you'll be good to go. Some people create the canvas element and attach it to an element passed in, but I prefer the other way, so it can be styled outside your code.

All in all, a commendable start. Have fun with it.

\$\endgroup\$
4
\$\begingroup\$

Cool. Just enjoyed playing it!

You might want to run it through JSHint maybe even get an editor that incorporates it. I use ScriptEd which is nice in that it's open source and it written in JS and runs on Node.js (so plenty of JavaScript there for you to play with :) The main thing JSHint pulls up is the unnecessary semicolons you've used on your function declarations, and you'll see a few other little things.

Node also gives you a nice command line environment to test out your logic offline.

Which reminds me the naming convention seems to be .js, so you might want to rename it snake.js :) I see there are plenty of other people who've gone down the same path so plenty of code to compare yours too.

As Guy mentions I'd agree with moving the 'snake' functionality into it's own object and module, and creating another 'utils' or 'view' module. And if you really want to get fancy, as you would only ever create one instance of the Snake object you could create it as a Singleton.

Nice use of nested functions, it took me a while before I started using those.

Anyhow I really enjoyed coming across this, as I'm also working on a version of TicTacToe.js so your canvas code will come in handy for that.

Look forward to 'fetch'ing the next version.

\$\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.