0
\$\begingroup\$

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

\$\endgroup\$
2
  • 2
    \$\begingroup\$ The code provided in question does nothing. Please provide a working copy of the code you wish reviewed in the question. (linking a repo does not count as reviewable code as it is subject to change) \$\endgroup\$
    – Blindman67
    May 24, 2021 at 11:02
  • 1
    \$\begingroup\$ To add to @Blindman67, this otherwise excellent question will be closed without working code \$\endgroup\$
    – konijn
    May 25, 2021 at 13:15

1 Answer 1

0
\$\begingroup\$

I do not pretend to be true like uncle Bob :)

However, I think your case you have to separate your controllers on main and another different.

preparation

app.js

const boardDrawerController = BoardDrawer(new BoardModel(), new BoardDrawerView(width, height));
const cellsController = CellsController(new CellsModel(), new CellsView());
const snakeController  = new SnakeController(new SnakeModel(), new SnakeView());
...

const mainController = new MainController(
boardDrawerController, 
cellsController,
snakeController
//another controllers
);

window.onload = () => {    
    mainController.run();
    startGame()
};

//MainController

  function run() {
     boardDrawerController.draw();
     cellsController.draw();
     snakeController.draw();
....
   }

Modal

Let's think what is modal? Modal has logic and view too. Back to app.js and create ModalController

 const modalController = ModalController(new ModalModel(), new ModalView());

What is Level, in simple - is only number 1,2,3 in general it is some logic without view (you can't draw Level you can redraw boardDrawer, Snake, Fruit another)

const Level = LevelController(new levelModel()); //without view

In finished we have MainController with simple controllers.

How communicate between controllers?

There are different ways, but in this simple case we can use pattern Observer.

//observer.js

class Observer {
  constructor () {
    this.observers = []
  }

  subscribe (fn) {
    this.observers.push(fn)
  }

  unsubscribe () {
    this.observers = [];
   
  }

  emit(data) {
    this.observers.forEach(subscriber => subscriber(data))
  }
}

//observable-of-controllers.js.

class ObservableOfControllers {
  static modalShow$ = new Observer(); 
  static levelChange$ = new Observer();
  

  constructor(modalController, levelController) {
   this.modalController = modalController;
   this.levelController = levelController;

 }
 
  subscribes() {
     ObservableOfControllers.modalShow$
                            .subscribe(data => this.this.modalController.show(data));
     
      ObservableOfControllers.levelChange$
                            .subscribe(data => this.this.levelController.change(data))
  }
}

add to app.js

const observableOfControllers = new ObservableOfControllers(
   modalController,
   levelController
);


window.onload = () => {    
    mainController.run();
    observableOfControllers.subscribes();
    startGame()
};

Emit event like be

class BoardModel {

   onError(message) {
     ObservableOfControllers.modalShow$.emit({
       type:  'error',
       message: message
     });

   }
}

I hope this simple example will help you understand MVC

\$\endgroup\$

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