I wanted to try out classes in Javascript and implemented a snake game. You can control the snake by using the arrow keys.
I would be interested to know if the source code can be improved in terms of class structure or source code structure in general. I never implemented snake in an other language and also didnt look into examples for doing that - so maybe this code seems a little bit odd to game programmers.
Class Structure
There are four Classes.
- Table: Manages the graphical display of a table and provides fundamental functions for changing the look of cells.
- Position: Represents a position with an x and an y coordinate.
- Snake: Manages "body parts" that are just a list of positions and can move and grow.
- GameBoard: Puts all these classes together to a game and also manages the graphical display via a Table instance.
There is also a function for checking which arrow key is pressed, and a function that repeatedly calls the mainloop of the gameboard-object.
<html>
<head>
<title>Snake</title>
<style>
table {
border: 1px solid;
}
td {
width: 25px;
height: 25px;
}
</style>
</head>
<body>
<div id="table"></div>
</body>
<script>
/* classes */
class Table {
rows;
columns;
tableNode;
constructor(rows, columns) {
this.rows = rows;
this.columns = columns;
}
initTableNode() {
this.tableNode = document.createElement("table");
for (let i = 0; i < this.rows; i++) {
let tr = document.createElement("tr");
for (let j = 0; j < this.columns; j++) {
let td = document.createElement("td");
tr.appendChild(td);
}
this.tableNode.appendChild(tr);
}
}
changeColor(x, y, color) {
if (x >= 0 && x < this.rows && y >= 0 && y < this.columns)
this.tableNode.rows[x].cells[y].style.background = color;
}
makeWhite() {
for (let i = 0; i < this.rows; i++) {
for (let j = 0; j < this.columns; j++) {
this.tableNode.rows[i].cells[j].style.background = "white";
}
}
}
}
class GameBoard {
table;
snake;
applePosition;
gameOver;
constructor(height, width) {
this.table = new Table(height, width);
this.snake = new Snake();
this.makeRandomApple();
}
makeRandomApple() {
start: while (true) {
let x = randomInt(0, this.table.rows-1);
let y = randomInt(0, this.table.columns-1);
this.applePosition = new Position(x, y);
// check if snake is already there
for (let i = 0; i < this.snake.bodyParts.length; i++) {
if (this.snake.bodyParts[i].x == x && this.snake.bodyParts[i].y == y) {
continue start;
}
}
return;
}
}
checkIfAppleIsEaten() {
if (this.snake.head().x == this.applePosition.x && this.snake.head().y == this.applePosition.y) {
this.snake.grow();
this.makeRandomApple();
}
}
gameLoop() {
this.snake.moveForward(direction);
this.checkIfAppleIsEaten();
// check if snake crashes into border
let head = this.snake.head();
if (head.x < 0 || head.x >= this.table.rows || head.y < 0 || head.y >= this.table.columns) {
this.gameOver = true;
}
// check if snakes crashes into himself
let bodyParts = 0;
for (let i = 0; i < this.snake.bodyParts.length; i++) {
if (head.x == this.snake.bodyParts[i].x && head.y == this.snake.bodyParts[i].y) {
bodyParts += 1;
}
if (bodyParts > 1) {
this.gameOver = true;
break;
}
}
this.table.makeWhite();
this.table.changeColor(this.applePosition.x, this.applePosition.y, "green");
// draw snake
for (let i = 0; i < this.snake.bodyParts.length; i++) {
this.table.changeColor(this.snake.bodyParts[i].x, this.snake.bodyParts[i].y, "red");
}
}
}
class Snake {
bodyParts;
// can contain strings: left, up, right, down
directionCurrently;
grown;
constructor() {
let p1 = new Position(1, 1);
let p2 = new Position(1, 2);
let p3 = new Position(1, 3);
let p4 = new Position(1, 4);
let p5 = new Position(1, 5);
this.bodyParts = [p1, p2, p3, p4, p5];
this.directionCurrently = "right";
this.grown = false;
}
grow() {
this.grown = true;
}
// direction can contain strings: left, up, right, down
moveForward(direction) {
let head = this.bodyParts[this.bodyParts.length-1]
let newHead;
if ((this.directionCurrently == "right" && direction != "left") ||
(this.directionCurrently == "left" && direction != "right") ||
(this.directionCurrently == "up" && direction != "down") ||
(this.directionCurrently == "down" && direction != "up")) {
this.directionCurrently = direction;
}
if (this.directionCurrently == "left") {
newHead = new Position(head.x, head.y-1);
} else if (this.directionCurrently == "up") {
newHead = new Position(head.x-1, head.y);
} else if (this.directionCurrently == "right") {
newHead = new Position(head.x, head.y+1);
} else if (this.directionCurrently == "down") {
newHead = new Position(head.x+1, head.y);
}
if (this.grown) {
this.grown = false;
} else {
this.bodyParts.shift();
}
this.bodyParts.push(newHead);
}
head() {
return this.bodyParts[this.bodyParts.length-1]
}
}
class Position {
x;
y;
constructor(x, y) {
this.x = x;
this.y = y;
}
equals(position) {
return this.x == x.position && this.y == y.position;
}
}
/* functions */
function randomInt(min, max) { // min and max included
return Math.floor(Math.random() * (max - min + 1) + min)
}
/* global variables */
let gameBoard = new GameBoard(10, 20);
let direction = "right"
const LEFT_KEY_CODE = 37;
const TOP_KEY_CODE = 38;
const RIGHT_KEY_CODE = 39;
const BOTTOM_KEY_CODE = 40;
/* commands, event listeners and timed routines */
// init html display
var divArea = document.getElementById("table");
gameBoard.table.initTableNode();
divArea.append(gameBoard.table.tableNode);
// start game
let gameLoop = window.setInterval(function() {
gameBoard.gameLoop();
if (gameBoard.gameOver) {
alert("Game over.");
divArea.removeChild(gameBoard.table.tableNode)
gameBoard = new GameBoard(10, 20);
gameBoard.table.initTableNode();
divArea.append(gameBoard.table.tableNode);
direction = "right";
}
}, 500);
// change direction by keyPress
document.onkeydown = function (event) {
if (event.keyCode == LEFT_KEY_CODE) {
direction = "left";
} else if (event.keyCode == TOP_KEY_CODE) {
direction = "up";
} else if (event.keyCode == RIGHT_KEY_CODE) {
direction = "right";
} else if (event.keyCode == BOTTOM_KEY_CODE) {
direction = "down";
}
}
</script>
</html>