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