javascript – Snake game MVC (Vanilla JS) – Help separating concerns

I’m making a snake game for practicing the MVC pattern and also going out of my comfort zone (I’m in data processing / data science, mostly procedural coding)

The game is available in https://nabla-f.github.io/

This is a classical snake game, but with obstacles in the board (called blocks in the code)

This is my first time implementing MVC, so all comments are appreciated. Also, I’m having some trouble implementing the following things, cause I doesn’t know where to put this functions (model, view or controller):

  • A level class (to be able to create different stages)
  • UI messages doesn’t related to game (“this works better in landscape mode”, “welcome to the game”, etc)
  • All things related to a future “app-alike” workflow (loading screens, auth screen, functions controlling this, etc)

I’m ommiting implementation details because my question are centered about sw design rather than implementation of each function (e.g. I know that using canvas is better than render html elements in each cycle, etc). Here’s the code:

models.js

const DIRECTIONS = {
    // an object representing directions (left, right, etc)
}

class Snake {
    constructor() {
        this.body = // Stores coordinates (an array) that represent snake position (i.e. an array of arrays)
        this.len = this.body.length 
        this.head = this.body(0)
        this.last = this.body(this.len-1)
        this.direction = // Direction in which snake moves
        this.toDigest = () // Stores coordinates that snake should add in the following cycle
    }

    updateDirection(direction) {
        this.direction = direction
    }

    updatePosition() {
        // Updates snakes body to a new position according to his direction            
    }

    updateData() {
        this.len = this.body.length
        this.head = this.body(0)
        this.last = this.body(this.len-1)
    }

    eatFruit(fruitCoord) {
        // Eats a fruit
    }

    checkToDigest() {
        // 
    }

    grow() {
        this.body.push(Array.from(this.toDigest(0)))
        this.toDigest.shift()
    }
}


class Board {
    constructor(width, height) {
        this.width = width
        this.height = height
        this.fruits = new Fruits()
        this.coords = // Stores coordinates of board. Each coordinate is an array
        this.blocks = // Stores coordinates of blocking elements
    }

    generateFruits(forbiddenCoords) {
        this.fruits.generateFruits()
    }
    
    removeFruits() {
        this.fruits.removeFruits()
    }
}


class Fruits {
    constructor() {
        this.fruits = () // Stores a list of fruits
    }

    generateFruits(maxWidth, maxHeight, forbiddenCoords) {
        // Generate a fruit coordinante that:
        // - Isn't the same as a block coordinate
        // - Isn't the same as a snake body coordinate
    }

    checkFruits() {
        if (this.fruits) {return true}
        else {return false}
    }

    removeFruits() {
        // Remove a fruit for the list of fruits
    }
}


export { Snake, Board, DIRECTIONS }

views.js


class BoardDrawer {
    constructor(HTMLelem, snake, board) {
        this.main = HTMLelem
        this.board = board
        this.snake = snake
    }

    drawCells() {
        // draw cells of the board
    }

    drawSnake() {
        // draws snake in board
    }

    drawBlocks() {
        // draws blocks in board
    }

    drawFruits() {
        // draws fruits in board
    }

    eraseSnake() {
        // erase the snake in board
    }

    eraseFruits() {
        // erase the fruits in board
    }
}


export { BoardDrawer }

controllers.js

import { DIRECTIONS } from './models.js'


class InputHandler {
    constructor(deniedKey) {
        this.deniedKey = deniedKey // last key pressed
    }
    
    userInput = (key) => {
       // translates an arrow key into direction coordinate
    }
}


class ObstaclesChecker {
    constructor(snake, board) {
        this.snake = snake.body
        this.boundaries = {
            left: -1,
            right: board.width,
            up: -1,
            down: board.height,
        }
        this.snakeWithoutHead = ()
        this.blocks = board.blocks
        this.fruits = board.fruits.fruits
    }

    checkBoundaryCollision(snakeHead) {
        // Checks if snake collides with boundaries
    }

    checkSelfCollision(snakeHead) {
        // Checks if snake collides with itself
    }

    checkBlockCollision(snakeHead) {
        // Checks if snake collides with a block
    }

    checkFruitCollision(snakeHead) {
        // Checks if snake collides with a fruit
    }
}


export { InputHandler, ObstaclesChecker }


app.js

import { Snake, Board } from './js/models.js'
import { BoardDrawer } from './js/views.js'
import { InputHandler, ObstaclesChecker } from './js/controllers.js'
 

// VARIABLE INIT

let boardHTMLelement = document.querySelector('#board');
let snake = new Snake()
let board = new Board(10, 10)
let obstacles = new ObstaclesChecker(snake, board)
let boardDrawer = new BoardDrawer(boardHTMLelement, snake, board)
let inputHandler = new InputHandler('ArrowLeft')


window.onload = () => {    

    // INITIALIZE GAME
    boardDrawer.drawCells()
    boardDrawer.drawBlocks()
    board.generateFruits()
    boardDrawer.drawFruits()
    boardDrawer.drawSnake()
    startGame()
};

/// GAME LOOP

function gameLoop(timeStamp){

    // SNAKE AND FRUITS LOGIC BLOCK
    
    boardDrawer.eraseSnake()
    boardDrawer.eraseFruits()

    // check digestion
    if (snake.checkToDigest()) {
        console.log('The snake has a new block')
        snake.updatePosition()
        snake.grow()
        snake.updateData()
    } else {
        snake.updatePosition()
        snake.updateData()
    }

    // check new fruit
    if (obstacles.checkFruitCollision(snake.head)) {
        console.log('The snake found a fruit')
        snake.eatFruit(snake.head)
        board.removeFruits()
        board.generateFruits((snake.toDigest, board.blocks))
    }

    // COLLISION CHECKING BLOCK
    if (
        obstacles.checkSelfCollision(snake.head) ||
        obstacles.checkBoundaryCollision(snake.head) ||
        obstacles.checkBlockCollision(snake.head)
        )
    {
        console.log('u lose')
        loserFunction()
    }
    
    // DRAWING BLOCK
    boardDrawer.drawFruits()
    boardDrawer.drawSnake()

    // Keep requesting new frames
    setTimeout(window.requestAnimationFrame, 250, gameLoop);    
}




/// Things that I doesn't know where to put in :(


let startGame = (event) => {
    if (('ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight').includes(event.key)) {
        window.requestAnimationFrame(gameLoop)
        document.removeEventListener('keydown', startGame)
    }
};

let startGameListener = () => {
    document.addEventListener('keydown', startGame)
}


const loserFunction = () => {
    // Delete the screen when user loses, show "you lose" message and so on...
}

If helps, here’s the repo: https://github.com/nabla-f/nabla-f.github.io

Many thanks in advance