Skip to main content
correct typo, make indentation under list item consistent
Source Link
  • Always use requestAnimationFrame to render any animation.

  • Keyboard Event.keyCode is a depreciated property and should not be used. Use Event.code or Event.key

  • To make the game more portable encapsulate the whole thing into a function.

  • Use a settings constant at the top of the code to keep all the game settings in one place, This makes it easier to make modifications and fine tune the game play.

  • Use lookups and mapping to simplify complex sets of conditions

  • The naming ifis too verbose. Names should be the minimum needed to understand, in context, what the name holds. eg getRandomInt the get is implied randomInt is better and does not clutter up code. getBetweenInt gives little information on what it does. randomIntBetween would be better.

  • DONT!!! use statement blocks without delimiting them. You invite very hard to spot bugs when later you make modification and forget the delimit the blocks.

ege.g.:

    // Never do
    if (foo) bar = 0;

    for (i = 0; i < 10; i ++) something(i);

    // Much safer to delimit them

    if (foo) { bar = 0 }   // NOTE that the ; is not needed if the line ends with }

    for (i = 0; i < 10; i ++) { something(i) }

let posMatch=(pos1,pos2)=>{ if(pos1.x==pos2.x&&pos1.y==pos2.y) return true; }

let posMatch=(pos1,pos2)=>{
    if(pos1.x==pos2.x&&pos1.y==pos2.y) return true;
}
  • Always use requestAnimationFrame to render any animation.

  • Keyboard Event.keyCode is a depreciated property and should not be used. Use Event.code or Event.key

  • To make the game more portable encapsulate the whole thing into a function.

  • Use a settings constant at the top of the code to keep all the game settings in one place, This makes it easier to make modifications and fine tune the game play.

  • Use lookups and mapping to simplify complex sets of conditions

  • The naming if too verbose. Names should be the minimum needed to understand, in context, what the name holds. eg getRandomInt the get is implied randomInt is better and does not clutter up code. getBetweenInt gives little information on what it does. randomIntBetween would be better.

  • DONT!!! use statement blocks without delimiting them. You invite very hard to spot bugs when later you make modification and forget the delimit the blocks.

eg

// Never do
if (foo) bar = 0;

for (i = 0; i < 10; i ++) something(i);

// Much safer to delimit them

if (foo) { bar = 0 }   // NOTE that the ; is not needed if the line ends with }

for (i = 0; i < 10; i ++) { something(i) }

let posMatch=(pos1,pos2)=>{ if(pos1.x==pos2.x&&pos1.y==pos2.y) return true; }

  • Always use requestAnimationFrame to render any animation.

  • Keyboard Event.keyCode is a depreciated property and should not be used. Use Event.code or Event.key

  • To make the game more portable encapsulate the whole thing into a function.

  • Use a settings constant at the top of the code to keep all the game settings in one place, This makes it easier to make modifications and fine tune the game play.

  • Use lookups and mapping to simplify complex sets of conditions

  • The naming is too verbose. Names should be the minimum needed to understand, in context, what the name holds. eg getRandomInt the get is implied randomInt is better and does not clutter up code. getBetweenInt gives little information on what it does. randomIntBetween would be better.

  • DONT!!! use statement blocks without delimiting them. You invite very hard to spot bugs when later you make modification and forget the delimit the blocks.

e.g.:

    // Never do
    if (foo) bar = 0;

    for (i = 0; i < 10; i ++) something(i);

    // Much safer to delimit them

    if (foo) { bar = 0 }   // NOTE that the ; is not needed if the line ends with }

    for (i = 0; i < 10; i ++) { something(i) }
let posMatch=(pos1,pos2)=>{
    if(pos1.x==pos2.x&&pos1.y==pos2.y) return true;
}
added 887 characters in body
Source Link
Blindman67
  • 22.9k
  • 2
  • 17
  • 40

Update There was a bug in the first post of this example that let the snake double back on its self if you quickly pressed two keys between moves.

To fix I added a pendingDir that holds the next direction until the next time the snake moves.

I also forgot to bind the point in the function snake.isOverPoint that prevented the snake running over its self.

    const snake = (() => {
        const col = settings.snakeColor;
        const body = [point()];
        const head = point(); // just a temp to do tests
        var length;
        const moves = settings.moves;
        var dir;
        var pendingDir;
        var index = 0;
        const each = cb => {index = 0; while (index < length) { cb(body[index++]) } };
        return {
            isOverPoint(point) { return arrayAny(point.isSame.bind(point), body, length) },
            set direction(value) { dirpendingDir = dir.op !== value ? moves[value] : dir },
            reset() {
                var i;
                dir = moves[randItem(settings.directions)];

                length = settings.snakeBodyMin;
                body[0].as(playfield.columns / 2 | 0, playfield.rows / 2 | 0);
                for (i = 1; i < length ; i++) {
                    if (body[i] === undefined) { body[i] = point() }
                    body[i].as(body[i-1]).add(moves[dir.op].vec);
                }
            },
            draw() {
                ctx.fillStyle = settings.snakeColor;
                each(playfield.fillBox);
            },
            update(canMove) {
                for (const dirName of settings.directions) { 
                    if (keys[moves[dirName].key]) { snake.direction = dirName } 
                }
                if (!canMove) { return }
                if (pendingDir !== undefined) { 
                    dir = pendingDir;
                    pendingDir = undefined;
                }
                
                head.as(body[0]).add(dir.vec);
                if (!snake.isOverPoint(head) && playfield.isInside(head)) {
                    if (food.isAt(head)) {
                        length += 1;
                        if (body.length <= length) { body.push(point()) }
                        game.score = settings.foodScore;
                        body.unshift(body.pop().as(head));
                        food.reset();
                    } else { body.unshift(body.pop().as(head)) }
                } else { game.state = "gameOver" }
            },
        };
    })();
    
;(() => {
    const randInt = (min = 2, max = min + (min = 0)) => Math.random() * (max - min) + min | 0;
    const randItem = array => array[Math.random() * array.length | 0];
    const point = (() => {
        function Point(x = 0, y = 0) { this.x = x; this.y = y }
        Point.prototype = {  // p is used to denote a point, Can have post fix number
            isSame(p) { return this.x === p.x && this.y === p.y },
            as(x, y = x.y + ((x = x.x), 0)) { return (this.x = x, this.y = y, this) },
            add(p) { this.x += p.x; this.y += p.y },
        }
        return (x, y) => new Point(x, y);
    })();
    const arrayAny = (predicate, array, count = array.length) => {
 
        for (const item of array) {
            if ( predicate(item) === true ) { return true }
            if (!(--count)) { break }
        }
        return false;
    }
    const settings = { // settings should always have the units and limits in the comments
        tiles : 10,  // in pixels size of a block. AKA multiplier from question
       
        snakeBodyMin : 5, // start number body parts
        snakeMoveEvery : 15, // number of frames between snake moves
        newGameWaitFrameCount : 60, // in frames 1/60th second
        foodScore : 1,
        foodColor : "yellow",
        backgroundColor : "gray",
        snakeColor : "white",
        directions : "up,down,left,right".split(","),
        moves : {   // mapping
            up : {name : "up", key : "ArrowUp", op : "down",  vec : point(0, -1)},
            down : {name : "down", key : "ArrowDown", op : "up", vec : point(0, 1)},
            left : {name : "left", key : "ArrowLeft", op : "right", vec : point(-1, 0)},
            right : {name : "right", key : "ArrowRight", op : "left",  vec : point(1, 0)},
        },
    }
    const keys = (() => {
        const keys = {any : false};
        for (const name of settings.directions) { keys[settings.moves[name].key] = false }
        function keyEvent(event) {
            if (keys[event.code] !== undefined) {
                keys[event.code] = event.type === "keydown";
                event.preventDefault();
                if (event.type === "keydown") { keys.any = true }

            }
        }
        addEventListener("keydown", keyEvent);
        addEventListener("keyup", keyEvent);
        focus(); // get focus
        return keys;
    })();
    keys.reset = () => {
        for (const keyName of settings.direction) { keys[settings.moves[keyName].key] = false }
    };

    canvas.style.background =  settings.backgroundColor;
    const ctx = canvas.getContext("2d");
    const playfield = (() => {
        const size = settings.tiles;
        const API = {
            columns : canvas.width / size,
            rows : canvas.height / size,
            fillBox(point, col = ctx.fillStyle) {
                ctx.fillStyle = col;
                ctx.fillRect(point.x * size, point.y * size, size, size);
            },
            isInside(p) { return ! (p.x < 0 || p.x >= API.rows || p.y < 0 || p.y >= API.columns) },
        };
        return API;
    })();
    const food = (() => {
        const col = settings.foodColor;
        const pos = point();
        return {
            reset() { // for performance dont use recursion
                var searching = true;   // Though not needed, some optimiser do not optimise code wrapped in a
                                        // loop with not exit condition eg while(true) so always include
                                        // an exit condition even if you dont need or use it
                while (searching) {
                    pos.x = randInt(playfield.columns);
                    pos.y = randInt(playfield.rows);
                    if (! snake.isOverPoint(pos)) {
                        searching = false;
                        break;
                    }
                }
            },
            draw() { playfield.fillBox(pos, col) },
            isAt(point) { return pos.isSame(point) },
        };
    })();
    const snake = (() => {
        const col = settings.snakeColor;
        const body = [point()];
        const head = point(); // just a temp to do tests
        var length;
        const moves = settings.moves;
        var dir;
        var pendingDir;
        var index = 0;
        const each = cb => {index = 0; while (index < length) { cb(body[index++]) } };
        return {
            isOverPoint(point) { return arrayAny(point.isSame.bind(point), body, length) },
            set direction(value) { dirpendingDir = dir.op !== value ? moves[value] : dir },
            reset() {
                var i;
                dir = moves[randItem(settings.directions)];

                length = settings.snakeBodyMin;
                body[0].as(playfield.columns / 2 | 0, playfield.rows / 2 | 0);
                for (i = 1; i < length ; i++) {
                    if (body[i] === undefined) { body[i] = point() }
                    body[i].as(body[i-1]).add(moves[dir.op].vec);
                }
            },
            draw() {
                ctx.fillStyle = settings.snakeColor;
                each(playfield.fillBox);
            },
            update(canMove) {
                for (const dirName of settings.directions) { if (keys[moves[dirName].key]) { snake.direction = dirName } }
                if (!canMove) { return }
                if (pendingDir !== undefined) { 
                    dir = pendingDir;
                    pendingDir = undefined;
                }
                
                
                head.as(body[0]).add(dir.vec);
                
                if (!snake.isOverPoint(head) && playfield.isInside(head)) {
                    if (food.isAt(head)) {
                        length += 1;
                        if (body.length <= length) { body.push(point()) }
                        game.score = settings.foodScore;
                        body.unshift(body.pop().as(head));
                        food.reset();
                    } else { body.unshift(body.pop().as(head)) }
                } else { game.state = "gameOver" }
            },
        };
    })();
    const game = (() => {
        var frameCount;
        var score = 0;
        var started = false;
        var currentName = ""; // not used ATM
        var current = null;
        var nextState;
        
        const game = {
            set score(value) {
                if (value === 0) { score = 0 }
                else { score += value }
                scoreElement.textContent = score;
            },
            set text(value){
                if (value === "") {
                    textElement.classList.add("hide");
                } else {
                    textElement.classList.remove("hide");
                    textElement.textContent = value;
                }
            },        
            newGame() {
                game.score = 0;
                snake.reset();
                food.reset();
                ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
                food.draw();
                snake.draw();
                nextState = "play";
                game.state = "waitForFrames";
                game.text = "Get ready";
                 
            },
            gameOver() {
                nextState = "newGame";
                game.state = "waitForFrames";
                game.text = "Game Over";
            },
            play() {
                ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
                snake.update(frameCount % settings.snakeMoveEvery === settings.snakeMoveEvery -1);
                food.draw();
                snake.draw();
            },
            waitForFrames() {
                if (frameCount >= settings.newGameWaitFrameCount) { game.state = nextState }
            },
            set state(stateName) {
                frameCount = 0;
                if (game[stateName] !== undefined) {
                    current = game[stateName];
                    currentName = stateName;
                } else {
                    current  = game.newGame;
                    currentName = "newGame";
                }
                game.text = "";
                if (!started) {
                    requestAnimationFrame(game.mainLoop);
                    started = true;
                }
               
            },
            mainLoop() {
                current();
                frameCount += 1;
                requestAnimationFrame(game.mainLoop)
            }
        }
        return game;
    })();
    // all done so start a new game;
    game.state = "newGame";
})();
body {
   font-family : arial;
}
canvas {
  position : absolute;
  top : 0px;
  left : 0px;  
  background : gray;
  z-index :-1;
}
#scoreElement {
  position : absolute;
  top : 4px;
  left : 4px;
}
#textElement {
  position : absolute;
  top : 100px;60px;
  left : 0px;  
  width :320px;160px;
  text-align : center;
}
.hide {
   display : none;
}
<canvas id="canvas" width="320"width="160" height="320">height="160"> </canvas>
<div id="scoreElement"></div>
<div class ="hide" id="textElement">dsdfsdfsf</div>
    const snake = (() => {
        const col = settings.snakeColor;
        const body = [point()];
        const head = point(); // just a temp to do tests
        var length;
        const moves = settings.moves;
        var dir;
        var index = 0;
        const each = cb => {index = 0; while (index < length) { cb(body[index++]) } };
        return {
            isOverPoint(point) { return arrayAny(point.isSame, body, length) },
            set direction(value) { dir = dir.op !== value ? moves[value] : dir },
            reset() {
                var i;
                dir = moves[randItem(settings.directions)];

                length = settings.snakeBodyMin;
                body[0].as(playfield.columns / 2 | 0, playfield.rows / 2 | 0);
                for (i = 1; i < length ; i++) {
                    if (body[i] === undefined) { body[i] = point() }
                    body[i].as(body[i-1]).add(moves[dir.op].vec);
                }
            },
            draw() {
                ctx.fillStyle = settings.snakeColor;
                each(playfield.fillBox);
            },
            update(canMove) {
                for (const dirName of settings.directions) { if (keys[moves[dirName].key]) { snake.direction = dirName } }
                if (!canMove) { return }
                
                head.as(body[0]).add(dir.vec);
                if (!snake.isOverPoint(head) && playfield.isInside(head)) {
                    if (food.isAt(head)) {
                        length += 1;
                        if (body.length <= length) { body.push(point()) }
                        game.score = settings.foodScore;
                        body.unshift(body.pop().as(head));
                        food.reset();
                    } else { body.unshift(body.pop().as(head)) }
                } else { game.state = "gameOver" }
            },
        };
    })();
    
;(() => {
    const randInt = (min = 2, max = min + (min = 0)) => Math.random() * (max - min) + min | 0;
    const randItem = array => array[Math.random() * array.length | 0];
    const point = (() => {
        function Point(x = 0, y = 0) { this.x = x; this.y = y }
        Point.prototype = {  // p is used to denote a point, Can have post fix number
            isSame(p) { return this.x === p.x && this.y === p.y },
            as(x, y = x.y + ((x = x.x), 0)) { return (this.x = x, this.y = y, this) },
            add(p) { this.x += p.x; this.y += p.y },
        }
        return (x, y) => new Point(x, y);
    })();
    const arrayAny = (predicate, array, count = array.length) => {
        for (const item of array) {
            if ( predicate(item) === true ) { return true }
            if (!(--count)) { break }
        }
        return false;
    }
    const settings = { // settings should always have the units and limits in the comments
        tiles : 10,  // in pixels size of a block. AKA multiplier from question
       
        snakeBodyMin : 5, // start number body parts
        snakeMoveEvery : 15, // number of frames between snake moves
        newGameWaitFrameCount : 60, // in frames 1/60th second
        foodScore : 1,
        foodColor : "yellow",
        backgroundColor : "gray",
        snakeColor : "white",
        directions : "up,down,left,right".split(","),
        moves : {   // mapping
            up : {name : "up", key : "ArrowUp", op : "down",  vec : point(0, -1)},
            down : {name : "down", key : "ArrowDown", op : "up", vec : point(0, 1)},
            left : {name : "left", key : "ArrowLeft", op : "right", vec : point(-1, 0)},
            right : {name : "right", key : "ArrowRight", op : "left",  vec : point(1, 0)},
        },
    }
    const keys = (() => {
        const keys = {any : false};
        for (const name of settings.directions) { keys[settings.moves[name].key] = false }
        function keyEvent(event) {
            if (keys[event.code] !== undefined) {
                keys[event.code] = event.type === "keydown";
                event.preventDefault();
                if (event.type === "keydown") { keys.any = true }

            }
        }
        addEventListener("keydown", keyEvent);
        addEventListener("keyup", keyEvent);
        focus(); // get focus
        return keys;
    })();
    keys.reset = () => {
        for (const keyName of settings.direction) { keys[settings.moves[keyName].key] = false }
    };

    canvas.style.background =  settings.backgroundColor;
    const ctx = canvas.getContext("2d");
    const playfield = (() => {
        const size = settings.tiles;
        const API = {
            columns : canvas.width / size,
            rows : canvas.height / size,
            fillBox(point, col = ctx.fillStyle) {
                ctx.fillStyle = col;
                ctx.fillRect(point.x * size, point.y * size, size, size);
            },
            isInside(p) { return ! (p.x < 0 || p.x >= API.rows || p.y < 0 || p.y >= API.columns) },
        };
        return API;
    })();
    const food = (() => {
        const col = settings.foodColor;
        const pos = point();
        return {
            reset() { // for performance dont use recursion
                var searching = true;   // Though not needed, some optimiser do not optimise code wrapped in a
                                        // loop with not exit condition eg while(true) so always include
                                        // an exit condition even if you dont need or use it
                while (searching) {
                    pos.x = randInt(playfield.columns);
                    pos.y = randInt(playfield.rows);
                    if (! snake.isOverPoint(pos)) {
                        searching = false;
                        break;
                    }
                }
            },
            draw() { playfield.fillBox(pos, col) },
            isAt(point) { return pos.isSame(point) },
        };
    })();
    const snake = (() => {
        const col = settings.snakeColor;
        const body = [point()];
        const head = point(); // just a temp to do tests
        var length;
        const moves = settings.moves;
        var dir;
        var index = 0;
        const each = cb => {index = 0; while (index < length) { cb(body[index++]) } };
        return {
            isOverPoint(point) { return arrayAny(point.isSame, body, length) },
            set direction(value) { dir = dir.op !== value ? moves[value] : dir },
            reset() {
                var i;
                dir = moves[randItem(settings.directions)];

                length = settings.snakeBodyMin;
                body[0].as(playfield.columns / 2 | 0, playfield.rows / 2 | 0);
                for (i = 1; i < length ; i++) {
                    if (body[i] === undefined) { body[i] = point() }
                    body[i].as(body[i-1]).add(moves[dir.op].vec);
                }
            },
            draw() {
                ctx.fillStyle = settings.snakeColor;
                each(playfield.fillBox);
            },
            update(canMove) {
                for (const dirName of settings.directions) { if (keys[moves[dirName].key]) { snake.direction = dirName } }
                if (!canMove) { return }
                
                head.as(body[0]).add(dir.vec);
                if (!snake.isOverPoint(head) && playfield.isInside(head)) {
                    if (food.isAt(head)) {
                        length += 1;
                        if (body.length <= length) { body.push(point()) }
                        game.score = settings.foodScore;
                        body.unshift(body.pop().as(head));
                        food.reset();
                    } else { body.unshift(body.pop().as(head)) }
                } else { game.state = "gameOver" }
            },
        };
    })();
    const game = (() => {
        var frameCount;
        var score = 0;
        var started = false;
        var currentName = ""; // not used ATM
        var current = null;
        var nextState;
        
        const game = {
            set score(value) {
                if (value === 0) { score = 0 }
                else { score += value }
                scoreElement.textContent = score;
            },
            set text(value){
                if (value === "") {
                    textElement.classList.add("hide");
                } else {
                    textElement.classList.remove("hide");
                    textElement.textContent = value;
                }
            },        
            newGame() {
                game.score = 0;
                snake.reset();
                food.reset();
                ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
                food.draw();
                snake.draw();
                nextState = "play";
                game.state = "waitForFrames";
                game.text = "Get ready";
                 
            },
            gameOver() {
                nextState = "newGame";
                game.state = "waitForFrames";
                game.text = "Game Over";
            },
            play() {
                ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
                snake.update(frameCount % settings.snakeMoveEvery === settings.snakeMoveEvery -1);
                food.draw();
                snake.draw();
            },
            waitForFrames() {
                if (frameCount >= settings.newGameWaitFrameCount) { game.state = nextState }
            },
            set state(stateName) {
                frameCount = 0;
                if (game[stateName] !== undefined) {
                    current = game[stateName];
                    currentName = stateName;
                } else {
                    current  = game.newGame;
                    currentName = "newGame";
                }
                game.text = "";
                if (!started) {
                    requestAnimationFrame(game.mainLoop);
                    started = true;
                }
               
            },
            mainLoop() {
                current();
                frameCount += 1;
                requestAnimationFrame(game.mainLoop)
            }
        }
        return game;
    })();
    // all done so start a new game;
    game.state = "newGame";
})();
body {
   font-family : arial;
}
canvas {
  position : absolute;
  top : 0px;
  left : 0px;  
  background : gray;
  z-index :-1;
}
#scoreElement {
  position : absolute;
  top : 4px;
  left : 4px;
}
#textElement {
  position : absolute;
  top : 100px;
  left : 0px;  
  width :320px;
  text-align : center;
}
.hide {
   display : none;
}
<canvas id="canvas" width="320" height="320"> </canvas>
<div id="scoreElement"></div>
<div class ="hide" id="textElement">dsdfsdfsf</div>

Update There was a bug in the first post of this example that let the snake double back on its self if you quickly pressed two keys between moves.

To fix I added a pendingDir that holds the next direction until the next time the snake moves.

I also forgot to bind the point in the function snake.isOverPoint that prevented the snake running over its self.

    const snake = (() => {
        const col = settings.snakeColor;
        const body = [point()];
        const head = point(); // just a temp to do tests
        var length;
        const moves = settings.moves;
        var dir;
        var pendingDir;
        var index = 0;
        const each = cb => {index = 0; while (index < length) { cb(body[index++]) } };
        return {
            isOverPoint(point) { return arrayAny(point.isSame.bind(point), body, length) },
            set direction(value) { pendingDir = dir.op !== value ? moves[value] : dir },
            reset() {
                var i;
                dir = moves[randItem(settings.directions)];

                length = settings.snakeBodyMin;
                body[0].as(playfield.columns / 2 | 0, playfield.rows / 2 | 0);
                for (i = 1; i < length ; i++) {
                    if (body[i] === undefined) { body[i] = point() }
                    body[i].as(body[i-1]).add(moves[dir.op].vec);
                }
            },
            draw() {
                ctx.fillStyle = settings.snakeColor;
                each(playfield.fillBox);
            },
            update(canMove) {
                for (const dirName of settings.directions) { 
                    if (keys[moves[dirName].key]) { snake.direction = dirName } 
                }
                if (!canMove) { return }
                if (pendingDir !== undefined) { 
                    dir = pendingDir;
                    pendingDir = undefined;
                }
                
                head.as(body[0]).add(dir.vec);
                if (!snake.isOverPoint(head) && playfield.isInside(head)) {
                    if (food.isAt(head)) {
                        length += 1;
                        if (body.length <= length) { body.push(point()) }
                        game.score = settings.foodScore;
                        body.unshift(body.pop().as(head));
                        food.reset();
                    } else { body.unshift(body.pop().as(head)) }
                } else { game.state = "gameOver" }
            },
        };
    })();
    
;(() => {
    const randInt = (min = 2, max = min + (min = 0)) => Math.random() * (max - min) + min | 0;
    const randItem = array => array[Math.random() * array.length | 0];
    const point = (() => {
        function Point(x = 0, y = 0) { this.x = x; this.y = y }
        Point.prototype = {  // p is used to denote a point, Can have post fix number
            isSame(p) { return this.x === p.x && this.y === p.y },
            as(x, y = x.y + ((x = x.x), 0)) { return (this.x = x, this.y = y, this) },
            add(p) { this.x += p.x; this.y += p.y },
        }
        return (x, y) => new Point(x, y);
    })();
    const arrayAny = (predicate, array, count = array.length) => {
 
        for (const item of array) {
            if ( predicate(item) === true ) { return true }
            if (!(--count)) { break }
        }
        return false;
    }
    const settings = { // settings should always have the units and limits in the comments
        tiles : 10,  // in pixels size of a block. AKA multiplier from question
       
        snakeBodyMin : 5, // start number body parts
        snakeMoveEvery : 15, // number of frames between snake moves
        newGameWaitFrameCount : 60, // in frames 1/60th second
        foodScore : 1,
        foodColor : "yellow",
        backgroundColor : "gray",
        snakeColor : "white",
        directions : "up,down,left,right".split(","),
        moves : {   // mapping
            up : {name : "up", key : "ArrowUp", op : "down",  vec : point(0, -1)},
            down : {name : "down", key : "ArrowDown", op : "up", vec : point(0, 1)},
            left : {name : "left", key : "ArrowLeft", op : "right", vec : point(-1, 0)},
            right : {name : "right", key : "ArrowRight", op : "left",  vec : point(1, 0)},
        },
    }
    const keys = (() => {
        const keys = {any : false};
        for (const name of settings.directions) { keys[settings.moves[name].key] = false }
        function keyEvent(event) {
            if (keys[event.code] !== undefined) {
                keys[event.code] = event.type === "keydown";
                event.preventDefault();
                if (event.type === "keydown") { keys.any = true }

            }
        }
        addEventListener("keydown", keyEvent);
        addEventListener("keyup", keyEvent);
        focus(); // get focus
        return keys;
    })();
    keys.reset = () => {
        for (const keyName of settings.direction) { keys[settings.moves[keyName].key] = false }
    };

    canvas.style.background =  settings.backgroundColor;
    const ctx = canvas.getContext("2d");
    const playfield = (() => {
        const size = settings.tiles;
        const API = {
            columns : canvas.width / size,
            rows : canvas.height / size,
            fillBox(point, col = ctx.fillStyle) {
                ctx.fillStyle = col;
                ctx.fillRect(point.x * size, point.y * size, size, size);
            },
            isInside(p) { return ! (p.x < 0 || p.x >= API.rows || p.y < 0 || p.y >= API.columns) },
        };
        return API;
    })();
    const food = (() => {
        const col = settings.foodColor;
        const pos = point();
        return {
            reset() { // for performance dont use recursion
                var searching = true;   // Though not needed, some optimiser do not optimise code wrapped in a
                                        // loop with not exit condition eg while(true) so always include
                                        // an exit condition even if you dont need or use it
                while (searching) {
                    pos.x = randInt(playfield.columns);
                    pos.y = randInt(playfield.rows);
                    if (! snake.isOverPoint(pos)) {
                        searching = false;
                        break;
                    }
                }
            },
            draw() { playfield.fillBox(pos, col) },
            isAt(point) { return pos.isSame(point) },
        };
    })();
    const snake = (() => {
        const col = settings.snakeColor;
        const body = [point()];
        const head = point(); // just a temp to do tests
        var length;
        const moves = settings.moves;
        var dir;
        var pendingDir;
        var index = 0;
        const each = cb => {index = 0; while (index < length) { cb(body[index++]) } };
        return {
            isOverPoint(point) { return arrayAny(point.isSame.bind(point), body, length) },
            set direction(value) { pendingDir = dir.op !== value ? moves[value] : dir },
            reset() {
                var i;
                dir = moves[randItem(settings.directions)];

                length = settings.snakeBodyMin;
                body[0].as(playfield.columns / 2 | 0, playfield.rows / 2 | 0);
                for (i = 1; i < length ; i++) {
                    if (body[i] === undefined) { body[i] = point() }
                    body[i].as(body[i-1]).add(moves[dir.op].vec);
                }
            },
            draw() {
                ctx.fillStyle = settings.snakeColor;
                each(playfield.fillBox);
            },
            update(canMove) {
                for (const dirName of settings.directions) { if (keys[moves[dirName].key]) { snake.direction = dirName } }
                if (!canMove) { return }
                if (pendingDir !== undefined) { 
                    dir = pendingDir;
                    pendingDir = undefined;
                }
                
                
                head.as(body[0]).add(dir.vec);
                
                if (!snake.isOverPoint(head) && playfield.isInside(head)) {
                    if (food.isAt(head)) {
                        length += 1;
                        if (body.length <= length) { body.push(point()) }
                        game.score = settings.foodScore;
                        body.unshift(body.pop().as(head));
                        food.reset();
                    } else { body.unshift(body.pop().as(head)) }
                } else { game.state = "gameOver" }
            },
        };
    })();
    const game = (() => {
        var frameCount;
        var score = 0;
        var started = false;
        var currentName = ""; // not used ATM
        var current = null;
        var nextState;
        
        const game = {
            set score(value) {
                if (value === 0) { score = 0 }
                else { score += value }
                scoreElement.textContent = score;
            },
            set text(value){
                if (value === "") {
                    textElement.classList.add("hide");
                } else {
                    textElement.classList.remove("hide");
                    textElement.textContent = value;
                }
            },        
            newGame() {
                game.score = 0;
                snake.reset();
                food.reset();
                ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
                food.draw();
                snake.draw();
                nextState = "play";
                game.state = "waitForFrames";
                game.text = "Get ready";
                 
            },
            gameOver() {
                nextState = "newGame";
                game.state = "waitForFrames";
                game.text = "Game Over";
            },
            play() {
                ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
                snake.update(frameCount % settings.snakeMoveEvery === settings.snakeMoveEvery -1);
                food.draw();
                snake.draw();
            },
            waitForFrames() {
                if (frameCount >= settings.newGameWaitFrameCount) { game.state = nextState }
            },
            set state(stateName) {
                frameCount = 0;
                if (game[stateName] !== undefined) {
                    current = game[stateName];
                    currentName = stateName;
                } else {
                    current  = game.newGame;
                    currentName = "newGame";
                }
                game.text = "";
                if (!started) {
                    requestAnimationFrame(game.mainLoop);
                    started = true;
                }
               
            },
            mainLoop() {
                current();
                frameCount += 1;
                requestAnimationFrame(game.mainLoop)
            }
        }
        return game;
    })();
    // all done so start a new game;
    game.state = "newGame";
})();
body {
   font-family : arial;
}
canvas {
  position : absolute;
  top : 0px;
  left : 0px;  
  background : gray;
  z-index :-1;
}
#scoreElement {
  position : absolute;
  top : 4px;
  left : 4px;
}
#textElement {
  position : absolute;
  top : 60px;
  left : 0px;  
  width :160px;
  text-align : center;
}
.hide {
   display : none;
}
<canvas id="canvas" width="160" height="160"> </canvas>
<div id="scoreElement"></div>
<div class ="hide" id="textElement">dsdfsdfsf</div>
deleted 98 characters in body
Source Link
Blindman67
  • 22.9k
  • 2
  • 17
  • 40
  • Always use requestAnimationFrame to render any animation.

  • Keyboard Event.keyCode is a depreciated property and should not be used. Use Event.codeEvent.code or Event.keyEvent.key

  • To make the game more portable encapsulate the whole thing into a function.

  • Use a settings constant at the top of the code to keep all the game settings in one place, This makes it easier to make modifications and fine tune the game play.

  • Use lookups and mapping to simplify complex sets of conditions

  • The naming if too verbose. Names should be the minimum needed to understand, in context, what the name holds. eg getRandomInt the getget is implied randomInt is better and does not clutter up code. getBetweenInt is too long and givegives little information on what it does. randomIntBetween would be better.

  • DONT!!! use statement blocks without delimiting them. You invite very hard to spot bugbugs when later you make modification and forget the delimit the blocks.

  • Reduce the number of function calls.
  • Reduce the number of new scopes
  • Generally function scope variables are quicker than many nested block scope variablevariables as block scope variables require heap allocation each time they enter scope. ThisHowever this is a very small overhead and only needs attention if you are pushing for max performance.
  • DON'T Delete, reuse. Is, is the number oneONE rule for reducing jank.
    const settings = { // settings should alway have the units and limits in the comments
        tiles : 10,  // Number ofin gamepixels, tilessize MUSTof BEa even.block, AKA multiplier from question
        frameTime: 200, // in ms (1/1000th)  Must be > 1000 / 60 aka speed from question
        
        snakeBodyMin : 5, // start number body parts
        snakeMoveEvery : 15, // number of frames between snake moves
        newGameWaitFrameCount : 60, // in frames 1/60th second
        foodScore : 1,
        foodColor : "yellow",
        backgroundColor : "gray", 
        snakeColor : "white",
    const keys = (() => {
        const keys = {any : false};
        for (const name of settings.directions) { keys[settings.moves[name].key] = false }
        function keyEvent(event) {
            if (keys[event.code] !== undefined) {
                keys[event.code] = event.type === "keydown";
                event.preventDefault();
                if (event.type === "keydown") { keys.any = true }

            }
        }
        addEventListener("keydown", keyEvent);
        addEventListener("keyup", keyEvent);
        document.focus(); // get focus
        return keys;
    })();
    keys.reset = () => {
        for (const keyName of settings.direction) { keys[settings.moves[keyName].key] = false }
    };
    const game = (() => {
        var frameCount;
        var score = 0;
        var started = false;
        var currentName = ""; // not used ATM
        var current = null;
        var nextState;
        
        const game = {
            set score(value) {
                if (value === 0) { score = 0 }
                else { score += value }
                scoreElement.textContent = score;
            },
            set text(value){
                if (value === "") {
                    textElement.classList.add("hide");
                } else {
                    textElement.classList.remove("hide");
                    textElement.textContent = value;
                }
            },        
            newGame() {
                game.score = 0;
                snake.reset();
                food.reset();
                ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
                food.draw();
                snake.draw();
                nextState = "play";
                game.state = "waitForFrames";
                game.text = "Get ready";
                 
            },
            gameOver() {
                nextState = "newGame";
                game.state = "waitForFrames";
                game.text = "Game Over";
            },
            play() {
                ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
                snake.update(frameCount % settings.snakeMoveEvery === settings.snakeMoveEvery -1);
                food.draw();
                snake.draw();
            },
            waitForFrames() {
                if (frameCount >= settings.newGameWaitFrameCount) {
                     game.state = nextStatenextState;
                }
            },
            set state(stateName) {
                frameCount = 0;
                if (game[stateName] !== undefined) {
                    current = game[stateName];
                    currentName = stateName;
                } else {
                    current  = game.newGame;
                    currentName = "newGame";
                }
                game.text = "";
                if (!started) {
                    requestAnimationFrame(game.mainLoop);
                    started = true;
                }
               
            },
            mainLoop() {
                current();
                frameCount += 1;
                requestAnimationFrame(game.mainLoop)
            }
        }
        return game;
    })();
    // all done so start a new game;
    game.state = "newGame";
})();
;(() => {
    const randInt = (min = 2, max = min + (min = 0)) => Math.random() * (max - min) + min | 0;
    const randItem = array => array[Math.random() * array.length | 0];
    const point = (() => {
        function Point(x = 0, y = 0) { this.x = x; this.y = y }
        Point.prototype = {  // p is used to denote a point, Can have post fix number
            isSame(p) { return this.x === p.x && this.y === p.y },
            as(x, y = x.y + ((x = x.x), 0)) { return (this.x = x, this.y = y, this) },
            add(p) { this.x += p.x; this.y += p.y },
        }
        return (x, y) => new Point(x, y);
    })();
    const arrayAny = (predicate, array, count = array.length) => {
        for (const item of array) {
            if ( predicate(item) === true ) { return true }
            if (!(--count)) { break }
        }
        return false;
    }
    const settings = { // settings should alwayalways have the units and limits in the comments
        tiles : 10,  // Number ofin gamepixels tilessize MUSTof BEa evenblock. AKA multiplier from question
        frameTime: 200, // in ms (1/1000th)  Must be > 1000 / 60 aka speed from question
        
        snakeBodyMin : 5, // start number body parts
        snakeMoveEvery : 15, // number of frames between snake moves
        newGameWaitFrameCount : 60, // in frames 1/60th second
        foodScore : 1,
        foodColor : "yellow",
        backgroundColor : "gray",
        snakeColor : "white",
        directions : "up,down,left,right".split(","),
        moves : {   // mapping
            up : {name : "up", key : "ArrowUp", op : "down",  vec : point(0, -1)},
            down : {name : "down", key : "ArrowDown", op : "up", vec : point(0, 1)},
            left : {name : "left", key : "ArrowLeft", op : "right", vec : point(-1, 0)},
            right : {name : "right", key : "ArrowRight", op : "left",  vec : point(1, 0)},
        },
    }
    const keys = (() => {
        const keys = {any : false};
        for (const name of settings.directions) { keys[settings.moves[name].key] = false }
        function keyEvent(event) {
            if (keys[event.code] !== undefined) {
                keys[event.code] = event.type === "keydown";
                event.preventDefault();
                if (event.type === "keydown") { keys.any = true }

            }
        }
        addEventListener("keydown", keyEvent);
        addEventListener("keyup", keyEvent);
        focus(); // get focus
        return keys;
    })();
    keys.reset = () => {
        for (const keyName of settings.direction) { keys[settings.moves[keyName].key] = false }
    };

    canvas.style.background =  settings.backgroundColor;
    const ctx = canvas.getContext("2d");
    const playfield = (() => {
        const size = settings.tiles;
        const API = {
            columns : canvas.width / size,
            rows : canvas.height / size,
            fillBox(point, col = ctx.fillStyle) {
                ctx.fillStyle = col;
                ctx.fillRect(point.x * size, point.y * size, size, size);
            },
            isInside(p) { return ! (p.x < 0 || p.x >= API.rows || p.y < 0 || p.y >= API.columns) },
        };
        return API;
    })();
    const food = (() => {
        const col = settings.foodColor;
        const pos = point();
        return {
            reset() { // for performance dont use recursion
                var searching = true;   // Though not needed, some optimiser do not optimise code wrapped in a
                                        // loop with not exit condition eg while(true) so always include
                                        // an exit condition even if you dont need or use it
                while (searching) {
                    pos.x = randInt(playfield.columns);
                    pos.y = randInt(playfield.rows);
                    if (! snake.isOverPoint(pos)) {
                        searching = false;
                        break;
                    }
                }
            },
            draw() { playfield.fillBox(pos, col) },
            isAt(point) { return pos.isSame(point) },
        };
    })();
    const snake = (() => {
        const col = settings.snakeColor;
        const body = [point()];
        const head = point(); // just a temp to do tests
        var length;
        const moves = settings.moves;
        var dir;
        var index = 0;
        const each = cb => {index = 0; while (index < length) { cb(body[index++]) } };
        return {
            isOverPoint(point) { return arrayAny(point.isSame, body, length) },
            set direction(value) { dir = dir.op !== value ? moves[value] : dir },
            reset() {
                var i;
                dir = moves[randItem(settings.directions)];

                length = settings.snakeBodyMin;
                body[0].as(playfield.columns / 2 | 0, playfield.rows / 2 | 0);
                for (i = 1; i < length ; i++) {
                    if (body[i] === undefined) { body[i] = point() }
                    body[i].as(body[i-1]).add(moves[dir.op].vec);
                }
            },
            draw() {
                ctx.fillStyle = settings.snakeColor;
                each(playfield.fillBox);
            },
            update(canMove) {
                for (const dirName of settings.directions) { if (keys[moves[dirName].key]) { snake.direction = dirName } }
                if (!canMove) { return }
                
                head.as(body[0]).add(dir.vec);
                if (!snake.isOverPoint(head) && playfield.isInside(head)) {
                    if (food.isAt(head)) {
                        length += 1;
                        if (body.length <= length) { body.push(point()) }
                        game.score = settings.foodScore;
                        body.unshift(body.pop().as(head));
                        food.reset();
                    } else { body.unshift(body.pop().as(head)) }
                } else { game.state = "gameOver" }
            },
        };
    })();
    const game = (() => {
        var frameCount;
        var score = 0;
        var started = false;
        var currentName = ""; // not used ATM
        var current = null;
        var nextState;
        
        const game = {
            set score(value) {
                if (value === 0) { score = 0 }
                else { score += value }
                scoreElement.textContent = score;
            },
            set text(value){
                if (value === "") {
                    textElement.classList.add("hide");
                } else {
                    textElement.classList.remove("hide");
                    textElement.textContent = value;
                }
            },        
            newGame() {
                game.score = 0;
                snake.reset();
                food.reset();
                ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
                food.draw();
                snake.draw();
                nextState = "play";
                game.state = "waitForFrames";
                game.text = "Get ready";
                 
            },
            gameOver() {
                nextState = "newGame";
                game.state = "waitForFrames";
                game.text = "Game Over";
            },
            play() {
                ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
                snake.update(frameCount % settings.snakeMoveEvery === settings.snakeMoveEvery -1);
                food.draw();
                snake.draw();
            },
            waitForFrames() {
                if (frameCount >= settings.newGameWaitFrameCount) { game.state = nextState }
            },
            set state(stateName) {
                frameCount = 0;
                if (game[stateName] !== undefined) {
                    current = game[stateName];
                    currentName = stateName;
                } else {
                    current  = game.newGame;
                    currentName = "newGame";
                }
                game.text = "";
                if (!started) {
                    requestAnimationFrame(game.mainLoop);
                    started = true;
                }
               
            },
            mainLoop() {
                current();
                frameCount += 1;
                requestAnimationFrame(game.mainLoop)
            }
        }
        return game;
    })();
    // all done so start a new game;
    game.state = "newGame";
})();
body {
   font-family : arial;
}
canvas {
  position : absolute;
  top : 0px;
  left : 0px;  
  background : gray;
  z-index :-1;
}
#scoreElement {
  position : absolute;
  top : 4px;
  left : 4px;
}
#textElement {
  position : absolute;
  top : 100px;
  left : 0px;  
  width :320px;
  text-align : center;
}
.hide {
   display : none;
}
<canvas id="canvas" width="320" height="320"> </canvas>
<div id="scoreElement"></div>
<div class ="hide" id="textElement">dsdfsdfsf</div>
  • Always use requestAnimationFrame to render any animation.

  • Keyboard Event.keyCode is a depreciated property and should not be used. Use Event.code or Event.key

  • To make the game more portable encapsulate the whole thing into a function.

  • Use a settings constant at the top of the code to keep all the game settings in one place, This makes it easier to make modifications and fine tune the game play.

  • Use lookups and mapping to simplify complex sets of conditions

  • The naming if too verbose. Names should be the minimum needed to understand, in context, what the name holds. eg getRandomInt the get is implied randomInt is better and does not clutter up code. getBetweenInt is too long and give little information on what it does. randomIntBetween would be better.

  • DONT!!! use statement blocks without delimiting them. You invite very hard to spot bug when later you make modification and forget the delimit the blocks.

  • Reduce the number of function calls.
  • Reduce the number of new scopes
  • Generally function scope variables are quicker than many nested block scope variable as block scope variables require heap allocation each time they enter scope. This is a very small overhead and only needs attention if you are pushing for max performance.
  • DON'T Delete, reuse. Is the number one rule for reducing jank.
    const settings = { // settings should alway have the units and limits in the comments
        tiles : 10,  // Number of game tiles MUST BE even. AKA multiplier from question
        frameTime: 200, // in ms (1/1000th)  Must be > 1000 / 60 aka speed from question
        
        snakeBodyMin : 5, // start number body parts
        snakeMoveEvery : 15, // number of frames between snake moves
        newGameWaitFrameCount : 60, // in frames 1/60th second
        foodScore : 1,
        foodColor : "yellow",
        backgroundColor : "gray",
        snakeColor : "white",
    const keys = (() => {
        const keys = {any : false};
        for (const name of settings.directions) { keys[settings.moves[name].key] = false }
        function keyEvent(event) {
            if (keys[event.code] !== undefined) {
                keys[event.code] = event.type === "keydown";
                event.preventDefault();
                if (event.type === "keydown") { keys.any = true }

            }
        }
        addEventListener("keydown", keyEvent);
        addEventListener("keyup", keyEvent);
        document.focus(); // get focus
        return keys;
    })();
    keys.reset = () => {
        for (const keyName of settings.direction) { keys[settings.moves[keyName].key] = false }
    };
    const game = (() => {
        var frameCount;
        var score = 0;
        var started = false;
        var currentName = ""; // not used ATM
        var current = null;
        var nextState;
        
        const game = {
            set score(value) {
                if (value === 0) { score = 0 }
                else { score += value }
                scoreElement.textContent = score;
            },
            set text(value){
                if (value === "") {
                    textElement.classList.add("hide");
                } else {
                    textElement.classList.remove("hide");
                    textElement.textContent = value;
                }
            },        
            newGame() {
                game.score = 0;
                snake.reset();
                food.reset();
                ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
                food.draw();
                snake.draw();
                nextState = "play";
                game.state = "waitForFrames";
                game.text = "Get ready";
                 
            },
            gameOver() {
                nextState = "newGame";
                game.state = "waitForFrames";
                game.text = "Game Over";
            },
            play() {
                ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
                snake.update(frameCount % settings.snakeMoveEvery === settings.snakeMoveEvery -1);
                food.draw();
                snake.draw();
            },
            waitForFrames() {
                if (frameCount >= settings.newGameWaitFrameCount) { game.state = nextState }
            },
            set state(stateName) {
                frameCount = 0;
                if (game[stateName] !== undefined) {
                    current = game[stateName];
                    currentName = stateName;
                } else {
                    current  = game.newGame;
                    currentName = "newGame";
                }
                game.text = "";
                if (!started) {
                    requestAnimationFrame(game.mainLoop);
                    started = true;
                }
               
            },
            mainLoop() {
                current();
                frameCount += 1;
                requestAnimationFrame(game.mainLoop)
            }
        }
        return game;
    })();
    // all done so start a new game;
    game.state = "newGame";
})();
;(() => {
    const randInt = (min = 2, max = min + (min = 0)) => Math.random() * (max - min) + min | 0;
    const randItem = array => array[Math.random() * array.length | 0];
    const point = (() => {
        function Point(x = 0, y = 0) { this.x = x; this.y = y }
        Point.prototype = {  // p is used to denote a point, Can have post fix number
            isSame(p) { return this.x === p.x && this.y === p.y },
            as(x, y = x.y + ((x = x.x), 0)) { return (this.x = x, this.y = y, this) },
            add(p) { this.x += p.x; this.y += p.y },
        }
        return (x, y) => new Point(x, y);
    })();
    const arrayAny = (predicate, array, count = array.length) => {
        for (const item of array) {
            if ( predicate(item) === true ) { return true }
            if (!(--count)) { break }
        }
        return false;
    }
    const settings = { // settings should alway have the units and limits in the comments
        tiles : 10,  // Number of game tiles MUST BE even. AKA multiplier from question
        frameTime: 200, // in ms (1/1000th)  Must be > 1000 / 60 aka speed from question
        
        snakeBodyMin : 5, // start number body parts
        snakeMoveEvery : 15, // number of frames between snake moves
        newGameWaitFrameCount : 60, // in frames 1/60th second
        foodScore : 1,
        foodColor : "yellow",
        backgroundColor : "gray",
        snakeColor : "white",
        directions : "up,down,left,right".split(","),
        moves : {   // mapping
            up : {name : "up", key : "ArrowUp", op : "down",  vec : point(0, -1)},
            down : {name : "down", key : "ArrowDown", op : "up", vec : point(0, 1)},
            left : {name : "left", key : "ArrowLeft", op : "right", vec : point(-1, 0)},
            right : {name : "right", key : "ArrowRight", op : "left",  vec : point(1, 0)},
        },
    }
    const keys = (() => {
        const keys = {any : false};
        for (const name of settings.directions) { keys[settings.moves[name].key] = false }
        function keyEvent(event) {
            if (keys[event.code] !== undefined) {
                keys[event.code] = event.type === "keydown";
                event.preventDefault();
                if (event.type === "keydown") { keys.any = true }

            }
        }
        addEventListener("keydown", keyEvent);
        addEventListener("keyup", keyEvent);
        focus(); // get focus
        return keys;
    })();
    keys.reset = () => {
        for (const keyName of settings.direction) { keys[settings.moves[keyName].key] = false }
    };

    const ctx = canvas.getContext("2d");
    const playfield = (() => {
        const size = settings.tiles;
        const API = {
            columns : canvas.width / size,
            rows : canvas.height / size,
            fillBox(point, col = ctx.fillStyle) {
                ctx.fillStyle = col;
                ctx.fillRect(point.x * size, point.y * size, size, size);
            },
            isInside(p) { return ! (p.x < 0 || p.x >= API.rows || p.y < 0 || p.y >= API.columns) },
        };
        return API;
    })();
    const food = (() => {
        const col = settings.foodColor;
        const pos = point();
        return {
            reset() { // for performance dont use recursion
                var searching = true;   // Though not needed, some optimiser do not optimise code wrapped in a
                                        // loop with not exit condition eg while(true) so always include
                                        // an exit condition even if you dont need or use it
                while (searching) {
                    pos.x = randInt(playfield.columns);
                    pos.y = randInt(playfield.rows);
                    if (! snake.isOverPoint(pos)) {
                        searching = false;
                        break;
                    }
                }
            },
            draw() { playfield.fillBox(pos, col) },
            isAt(point) { return pos.isSame(point) },
        };
    })();
    const snake = (() => {
        const col = settings.snakeColor;
        const body = [point()];
        const head = point(); // just a temp to do tests
        var length;
        const moves = settings.moves;
        var dir;
        var index = 0;
        const each = cb => {index = 0; while (index < length) { cb(body[index++]) } };
        return {
            isOverPoint(point) { return arrayAny(point.isSame, body, length) },
            set direction(value) { dir = dir.op !== value ? moves[value] : dir },
            reset() {
                var i;
                dir = moves[randItem(settings.directions)];

                length = settings.snakeBodyMin;
                body[0].as(playfield.columns / 2 | 0, playfield.rows / 2 | 0);
                for (i = 1; i < length ; i++) {
                    if (body[i] === undefined) { body[i] = point() }
                    body[i].as(body[i-1]).add(moves[dir.op].vec);
                }
            },
            draw() {
                ctx.fillStyle = settings.snakeColor;
                each(playfield.fillBox);
            },
            update(canMove) {
                for (const dirName of settings.directions) { if (keys[moves[dirName].key]) { snake.direction = dirName } }
                if (!canMove) { return }
                
                head.as(body[0]).add(dir.vec);
                if (!snake.isOverPoint(head) && playfield.isInside(head)) {
                    if (food.isAt(head)) {
                        length += 1;
                        if (body.length <= length) { body.push(point()) }
                        game.score = settings.foodScore;
                        body.unshift(body.pop().as(head));
                        food.reset();
                    } else { body.unshift(body.pop().as(head)) }
                } else { game.state = "gameOver" }
            },
        };
    })();
    const game = (() => {
        var frameCount;
        var score = 0;
        var started = false;
        var currentName = ""; // not used ATM
        var current = null;
        var nextState;
        
        const game = {
            set score(value) {
                if (value === 0) { score = 0 }
                else { score += value }
                scoreElement.textContent = score;
            },
            set text(value){
                if (value === "") {
                    textElement.classList.add("hide");
                } else {
                    textElement.classList.remove("hide");
                    textElement.textContent = value;
                }
            },        
            newGame() {
                game.score = 0;
                snake.reset();
                food.reset();
                ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
                food.draw();
                snake.draw();
                nextState = "play";
                game.state = "waitForFrames";
                game.text = "Get ready";
                 
            },
            gameOver() {
                nextState = "newGame";
                game.state = "waitForFrames";
                game.text = "Game Over";
            },
            play() {
                ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
                snake.update(frameCount % settings.snakeMoveEvery === settings.snakeMoveEvery -1);
                food.draw();
                snake.draw();
            },
            waitForFrames() {
                if (frameCount >= settings.newGameWaitFrameCount) { game.state = nextState }
            },
            set state(stateName) {
                frameCount = 0;
                if (game[stateName] !== undefined) {
                    current = game[stateName];
                    currentName = stateName;
                } else {
                    current  = game.newGame;
                    currentName = "newGame";
                }
                game.text = "";
                if (!started) {
                    requestAnimationFrame(game.mainLoop);
                    started = true;
                }
               
            },
            mainLoop() {
                current();
                frameCount += 1;
                requestAnimationFrame(game.mainLoop)
            }
        }
        return game;
    })();
    // all done so start a new game;
    game.state = "newGame";
})();
body {
   font-family : arial;
}
canvas {
  position : absolute;
  top : 0px;
  left : 0px;  
  background : gray;
  z-index :-1;
}
#scoreElement {
  position : absolute;
  top : 4px;
  left : 4px;
}
#textElement {
  position : absolute;
  top : 100px;
  left : 0px;  
  width :320px;
  text-align : center;
}
.hide {
   display : none;
}
<canvas id="canvas" width="320" height="320"> </canvas>
<div id="scoreElement"></div>
<div class ="hide" id="textElement">dsdfsdfsf</div>
  • Always use requestAnimationFrame to render any animation.

  • Keyboard Event.keyCode is a depreciated property and should not be used. Use Event.code or Event.key

  • To make the game more portable encapsulate the whole thing into a function.

  • Use a settings constant at the top of the code to keep all the game settings in one place, This makes it easier to make modifications and fine tune the game play.

  • Use lookups and mapping to simplify complex sets of conditions

  • The naming if too verbose. Names should be the minimum needed to understand, in context, what the name holds. eg getRandomInt the get is implied randomInt is better and does not clutter up code. getBetweenInt gives little information on what it does. randomIntBetween would be better.

  • DONT!!! use statement blocks without delimiting them. You invite very hard to spot bugs when later you make modification and forget the delimit the blocks.

  • Reduce the number of function calls.
  • Reduce the number of new scopes
  • Generally function scope variables are quicker than many nested block scope variables as block scope variables require heap allocation each time they enter scope. However this is a very small overhead and only needs attention if you are pushing for max performance.
  • DON'T Delete, reuse, is the number ONE rule for reducing jank.
    const settings = { // settings should alway have the units and limits in the comments
        tiles : 10,  // in pixels, size of a block, AKA multiplier from question

        
        snakeBodyMin : 5, // start number body parts
        snakeMoveEvery : 15, // number of frames between snake moves
        newGameWaitFrameCount : 60, // in frames 1/60th second
        foodScore : 1,
        foodColor : "yellow",
        backgroundColor : "gray", 
        snakeColor : "white",
    const keys = (() => {
        const keys = {any : false};
        for (const name of settings.directions) { keys[settings.moves[name].key] = false }
        function keyEvent(event) {
            if (keys[event.code] !== undefined) {
                keys[event.code] = event.type === "keydown";
                event.preventDefault();
                if (event.type === "keydown") { keys.any = true }

            }
        }
        addEventListener("keydown", keyEvent);
        addEventListener("keyup", keyEvent);
        focus(); // get focus
        return keys;
    })();
    keys.reset = () => {
        for (const keyName of settings.direction) { keys[settings.moves[keyName].key] = false }
    };
    const game = (() => {
        var frameCount;
        var score = 0;
        var started = false;
        var currentName = ""; // not used ATM
        var current = null;
        var nextState;
        
        const game = {
            set score(value) {
                if (value === 0) { score = 0 }
                else { score += value }
                scoreElement.textContent = score;
            },
            set text(value){
                if (value === "") {
                    textElement.classList.add("hide");
                } else {
                    textElement.classList.remove("hide");
                    textElement.textContent = value;
                }
            },        
            newGame() {
                game.score = 0;
                snake.reset();
                food.reset();
                ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
                food.draw();
                snake.draw();
                nextState = "play";
                game.state = "waitForFrames";
                game.text = "Get ready";
                 
            },
            gameOver() {
                nextState = "newGame";
                game.state = "waitForFrames";
                game.text = "Game Over";
            },
            play() {
                ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
                snake.update(frameCount % settings.snakeMoveEvery === settings.snakeMoveEvery -1);
                food.draw();
                snake.draw();
            },
            waitForFrames() {
                if (frameCount >= settings.newGameWaitFrameCount) {
                     game.state = nextState;
                }
            },
            set state(stateName) {
                frameCount = 0;
                if (game[stateName] !== undefined) {
                    current = game[stateName];
                    currentName = stateName;
                } else {
                    current  = game.newGame;
                    currentName = "newGame";
                }
                game.text = "";
                if (!started) {
                    requestAnimationFrame(game.mainLoop);
                    started = true;
                }
               
            },
            mainLoop() {
                current();
                frameCount += 1;
                requestAnimationFrame(game.mainLoop)
            }
        }
        return game;
    })();
    // all done so start a new game;
    game.state = "newGame";
})();
;(() => {
    const randInt = (min = 2, max = min + (min = 0)) => Math.random() * (max - min) + min | 0;
    const randItem = array => array[Math.random() * array.length | 0];
    const point = (() => {
        function Point(x = 0, y = 0) { this.x = x; this.y = y }
        Point.prototype = {  // p is used to denote a point, Can have post fix number
            isSame(p) { return this.x === p.x && this.y === p.y },
            as(x, y = x.y + ((x = x.x), 0)) { return (this.x = x, this.y = y, this) },
            add(p) { this.x += p.x; this.y += p.y },
        }
        return (x, y) => new Point(x, y);
    })();
    const arrayAny = (predicate, array, count = array.length) => {
        for (const item of array) {
            if ( predicate(item) === true ) { return true }
            if (!(--count)) { break }
        }
        return false;
    }
    const settings = { // settings should always have the units and limits in the comments
        tiles : 10,  // in pixels size of a block. AKA multiplier from question
       
        snakeBodyMin : 5, // start number body parts
        snakeMoveEvery : 15, // number of frames between snake moves
        newGameWaitFrameCount : 60, // in frames 1/60th second
        foodScore : 1,
        foodColor : "yellow",
        backgroundColor : "gray",
        snakeColor : "white",
        directions : "up,down,left,right".split(","),
        moves : {   // mapping
            up : {name : "up", key : "ArrowUp", op : "down",  vec : point(0, -1)},
            down : {name : "down", key : "ArrowDown", op : "up", vec : point(0, 1)},
            left : {name : "left", key : "ArrowLeft", op : "right", vec : point(-1, 0)},
            right : {name : "right", key : "ArrowRight", op : "left",  vec : point(1, 0)},
        },
    }
    const keys = (() => {
        const keys = {any : false};
        for (const name of settings.directions) { keys[settings.moves[name].key] = false }
        function keyEvent(event) {
            if (keys[event.code] !== undefined) {
                keys[event.code] = event.type === "keydown";
                event.preventDefault();
                if (event.type === "keydown") { keys.any = true }

            }
        }
        addEventListener("keydown", keyEvent);
        addEventListener("keyup", keyEvent);
        focus(); // get focus
        return keys;
    })();
    keys.reset = () => {
        for (const keyName of settings.direction) { keys[settings.moves[keyName].key] = false }
    };

    canvas.style.background =  settings.backgroundColor;
    const ctx = canvas.getContext("2d");
    const playfield = (() => {
        const size = settings.tiles;
        const API = {
            columns : canvas.width / size,
            rows : canvas.height / size,
            fillBox(point, col = ctx.fillStyle) {
                ctx.fillStyle = col;
                ctx.fillRect(point.x * size, point.y * size, size, size);
            },
            isInside(p) { return ! (p.x < 0 || p.x >= API.rows || p.y < 0 || p.y >= API.columns) },
        };
        return API;
    })();
    const food = (() => {
        const col = settings.foodColor;
        const pos = point();
        return {
            reset() { // for performance dont use recursion
                var searching = true;   // Though not needed, some optimiser do not optimise code wrapped in a
                                        // loop with not exit condition eg while(true) so always include
                                        // an exit condition even if you dont need or use it
                while (searching) {
                    pos.x = randInt(playfield.columns);
                    pos.y = randInt(playfield.rows);
                    if (! snake.isOverPoint(pos)) {
                        searching = false;
                        break;
                    }
                }
            },
            draw() { playfield.fillBox(pos, col) },
            isAt(point) { return pos.isSame(point) },
        };
    })();
    const snake = (() => {
        const col = settings.snakeColor;
        const body = [point()];
        const head = point(); // just a temp to do tests
        var length;
        const moves = settings.moves;
        var dir;
        var index = 0;
        const each = cb => {index = 0; while (index < length) { cb(body[index++]) } };
        return {
            isOverPoint(point) { return arrayAny(point.isSame, body, length) },
            set direction(value) { dir = dir.op !== value ? moves[value] : dir },
            reset() {
                var i;
                dir = moves[randItem(settings.directions)];

                length = settings.snakeBodyMin;
                body[0].as(playfield.columns / 2 | 0, playfield.rows / 2 | 0);
                for (i = 1; i < length ; i++) {
                    if (body[i] === undefined) { body[i] = point() }
                    body[i].as(body[i-1]).add(moves[dir.op].vec);
                }
            },
            draw() {
                ctx.fillStyle = settings.snakeColor;
                each(playfield.fillBox);
            },
            update(canMove) {
                for (const dirName of settings.directions) { if (keys[moves[dirName].key]) { snake.direction = dirName } }
                if (!canMove) { return }
                
                head.as(body[0]).add(dir.vec);
                if (!snake.isOverPoint(head) && playfield.isInside(head)) {
                    if (food.isAt(head)) {
                        length += 1;
                        if (body.length <= length) { body.push(point()) }
                        game.score = settings.foodScore;
                        body.unshift(body.pop().as(head));
                        food.reset();
                    } else { body.unshift(body.pop().as(head)) }
                } else { game.state = "gameOver" }
            },
        };
    })();
    const game = (() => {
        var frameCount;
        var score = 0;
        var started = false;
        var currentName = ""; // not used ATM
        var current = null;
        var nextState;
        
        const game = {
            set score(value) {
                if (value === 0) { score = 0 }
                else { score += value }
                scoreElement.textContent = score;
            },
            set text(value){
                if (value === "") {
                    textElement.classList.add("hide");
                } else {
                    textElement.classList.remove("hide");
                    textElement.textContent = value;
                }
            },        
            newGame() {
                game.score = 0;
                snake.reset();
                food.reset();
                ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
                food.draw();
                snake.draw();
                nextState = "play";
                game.state = "waitForFrames";
                game.text = "Get ready";
                 
            },
            gameOver() {
                nextState = "newGame";
                game.state = "waitForFrames";
                game.text = "Game Over";
            },
            play() {
                ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
                snake.update(frameCount % settings.snakeMoveEvery === settings.snakeMoveEvery -1);
                food.draw();
                snake.draw();
            },
            waitForFrames() {
                if (frameCount >= settings.newGameWaitFrameCount) { game.state = nextState }
            },
            set state(stateName) {
                frameCount = 0;
                if (game[stateName] !== undefined) {
                    current = game[stateName];
                    currentName = stateName;
                } else {
                    current  = game.newGame;
                    currentName = "newGame";
                }
                game.text = "";
                if (!started) {
                    requestAnimationFrame(game.mainLoop);
                    started = true;
                }
               
            },
            mainLoop() {
                current();
                frameCount += 1;
                requestAnimationFrame(game.mainLoop)
            }
        }
        return game;
    })();
    // all done so start a new game;
    game.state = "newGame";
})();
body {
   font-family : arial;
}
canvas {
  position : absolute;
  top : 0px;
  left : 0px;  
  background : gray;
  z-index :-1;
}
#scoreElement {
  position : absolute;
  top : 4px;
  left : 4px;
}
#textElement {
  position : absolute;
  top : 100px;
  left : 0px;  
  width :320px;
  text-align : center;
}
.hide {
   display : none;
}
<canvas id="canvas" width="320" height="320"> </canvas>
<div id="scoreElement"></div>
<div class ="hide" id="textElement">dsdfsdfsf</div>
Source Link
Blindman67
  • 22.9k
  • 2
  • 17
  • 40
Loading