I feel like there's something wrong with the way my code is styled but I can't quite place what's wrong with it. Perhaps the way I've represented the snake is a little convoluted?
const HEIGHT = 20;
const WIDTH = 20;
const snake = function() {
const snakeCoordinates = [[0, 0], [1, 0]];
const moveDirections = [[-1, 0], [-1, 0]];
function shiftDirections() {
for (let i = snakeCoordinates.length; i > 0; i--)
moveDirections[i] = moveDirections[i - 1];
}
return {
hasSnake: function(row, col) {
return snakeCoordinates.reduce((accum, [snakeRow, snakeCol]) => accum || (row === snakeRow && col === snakeCol), false);
},
expandSnake: function() {
const head = snakeCoordinates[0];
const headDirection = moveDirections[0];
snakeCoordinates.unshift(wrapAround([head[0] + headDirection[0], head[1] + headDirection[1]]));
moveDirections.unshift(headDirection);
},
changeDirection: function(x, y) {
const isAboutTurn = x === -moveDirections[0][0] || y === -moveDirections[0][1];
if (!isAboutTurn)
moveDirections[0] = [x, y];
},
getSnakeHead: function() {
return snakeCoordinates[0];
},
isSnakeDead: function() {
const tail = snakeCoordinates.slice(1);
const head = snakeCoordinates[0];
return tail.reduce((accum, [row, col]) => accum || (row === head[0] && col === head[1]), false);
},
moveSnake: function() {
for (let i = 0; i < snakeCoordinates.length; i++) {
snakeCoordinates[i][0] += moveDirections[i][0];
snakeCoordinates[i][1] += moveDirections[i][1];
snakeCoordinates[i] = wrapAround(snakeCoordinates[i]);
}
shiftDirections();
}
};
}();
const goal = function() {
var goal = [HEIGHT - 1, WIDTH - 1];
return {
newGoal: function() {
goal = [rng(0, HEIGHT), rng(0, WIDTH)];
},
isGoal: function(row, col) {
return goal[0] === row && goal[1] === col;
}
};
}();
initialiseGame();
function initialiseGame() {
initialiseGridInDOM();
addEventListeners();
runGame();
}
function addEventListeners() {
document.addEventListener("keydown", dealWithKeyPress);
}
// Map key press onto action
function dealWithKeyPress(keyPress) {
const leftArrow = 37;
const upArrow = 38;
const rightArrow = 39;
const downArrow = 40;
switch (keyPress.keyCode) {
case upArrow:
snake.changeDirection(-1, 0);
break;
case leftArrow:
snake.changeDirection(0, -1);
break;
case rightArrow:
snake.changeDirection(0, 1);
break;
case downArrow:
snake.changeDirection(1, 0);
break;
}
}
async function runGame() {
var running = true;
while (running) {
snake.moveSnake();
checkForSnakeDeath(gameOver);
checkForGoalCapture();
updateDOM();
await delay();
}
function gameOver() {
alert("Game over!");
running = false;
}
}
function checkForSnakeDeath(gameOver) {
if (snake.isSnakeDead())
gameOver();
}
function checkForGoalCapture() {
const head = snake.getSnakeHead();
if (goal.isGoal(head[0], head[1])) {
snake.expandSnake();
goal.newGoal();
}
}
function delay() {
return new Promise(resolve => {
setTimeout(resolve, 60);
});
}
function updateDOM() {
for (let row = 0; row < HEIGHT; row++)
for (let col = 0; col < WIDTH; col++)
if (snake.hasSnake(row, col))
colorSnakeInDOM(row, col);
else if (goal.isGoal(row, col))
colorGoalInDOM(row, col);
else
colorWallInDOM(row, col);
}
function colorSnakeInDOM(row, col) {
const SNAKE_COLOR = "#00BFFF";
colorTileInDOM(SNAKE_COLOR, row, col);
}
function colorWallInDOM(row, col) {
const WALL_COLOR = selectBackgroundColor(row, col);
colorTileInDOM(WALL_COLOR, row, col);
}
function colorGoalInDOM(row, col) {
const GOAL_COLOR = "yellow";
colorTileInDOM(GOAL_COLOR, row, col);
}
function colorTileInDOM(color, row, col) {
const tileDOM = getTileInDOM(row, col);
tileDOM.style.backgroundColor = color;
}
function getTileInDOM(row, col) {
const gridDOM = document.querySelector("#grid");
const rowDOM = gridDOM.rows[row];
const tileDOM = rowDOM.cells[col];
return tileDOM;
}
// Dynamically generate HTML for a plain grid
function initialiseGridInDOM() {
const gridDOM = document.querySelector("#grid");
for (let row = 0; row < HEIGHT; row++) {
let newRow = createEmptyRowInDOM(row);
gridDOM.append(newRow);
}
updateDOM();
}
function createEmptyRowInDOM(row) {
const newRow = document.createElement("tr");
newRow.className = "row";
for (let col = 0; col < WIDTH; col++) {
let newTile = createEmptyTileInDOM(row, col);
newRow.append(newTile);
}
return newRow;
}
function createEmptyTileInDOM(row, col) {
const newTile = document.createElement("td");
newTile.className = "tile";
return newTile;
}
function selectBackgroundColor(row, col) {
const BACKGROUND_COLOR1 = "#3dfc03";
const BACKGROUND_COLOR2 = "#03fc03";
if ((row + col) % 2 === 0)
return BACKGROUND_COLOR1;
else
return BACKGROUND_COLOR2;
}
function wrapAround([row, col]) {
if (row === HEIGHT)
row = 0;
if (row < 0)
row = HEIGHT - 1;
if (col === WIDTH)
col = 0;
if (col < 0)
col = WIDTH - 1;
return [row, col];
}
// Generates a random number whose value lies between lower and upper
function rng(lower, upper) {
return Math.floor(Math.random() * (upper - lower)) + lower;
}
```
1 Answer 1
First, please be indulgent, english is not my native langage :).
Here is my take on your script. I will comment on the modifications I have made.
I have separated your script into chapters (that can lead to separated files if you want) :
- Game logic (snake and goal)
- Game flow (start, loop, game over)
- Controls
- Rendering
I have made some variable alignment and used ES6 shortened method definition (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/Method_definitions)
GAME LOGIC
For the snake object I have changed the self executing function to a named function for a better readability.
I have also shortened the method names because we already know we are on the snake object.
For example, I changed snake.expandSnake() to snake.expand().
The reduce onliners were a bit hard to read, so I changed them to multi line.
const HEIGHT = 20
const WIDTH = 20
// GAME LOGIC
function createSnake () {
const coordinates = [[0, 0], [1, 0]]
const moveDirections = [[-1, 0], [-1, 0]]
function shiftDirections () {
for (let i = coordinates.length; i > 0; i--) {
moveDirections[i] = moveDirections[i - 1]
}
}
return {
hasSnake (row, col) {
return coordinates.reduce(function (accum, [snakeRow, snakeCol]) {
return accum || (row === snakeRow && col === snakeCol)
}, false)
},
expand () {
const head = coordinates[0]
const headDirection = moveDirections[0]
coordinates.unshift(
wrapAround([
head[0] + headDirection[0],
head[1] + headDirection[1]
])
)
moveDirections.unshift(headDirection)
},
changeDirection (x, y) {
const isAboutTurn = (
x === -moveDirections[0][0] ||
y === -moveDirections[0][1]
)
if (!isAboutTurn) {
moveDirections[0] = [x, y]
}
},
getHead () {
return coordinates[0]
},
isDead () {
const tail = coordinates.slice(1)
const head = coordinates[0]
return tail.reduce(function (accum, [row, col]) {
return accum || (row === head[0] && col === head[1])
}, false)
},
move () {
for (let i = 0; i < coordinates.length; i++) {
coordinates[i][0] += moveDirections[i][0]
coordinates[i][1] += moveDirections[i][1]
coordinates[i] = wrapAround(coordinates[i])
}
shiftDirections()
}
}
}
function createGoal () {
let coords = [HEIGHT - 1, WIDTH - 1]
return {
newGoal: function () {
coords = [randomIntBetween(0, HEIGHT), randomIntBetween(0, WIDTH)]
},
isGoal: function (row, col) {
return coords[0] === row && coords[1] === col
}
}
}
GAME FLOW
You can see the objects snake and goal are now created in this chapter.
I have renamed checkForSnakeDeath and checkForGoalCapture into handleSnakeDeath and handleGoal, because what you are doing in thoses functions is more than a simple check. In my opinion, a check should just return true or false, or maybe an error object but not doing actions.
// GAME FLOW
let snake = createSnake()
let goal = createGoal()
initialiseGame()
function initialiseGame () {
initialiseDOMGrid()
addEventListeners()
runGame()
}
function addEventListeners () {
document.addEventListener('keydown', moveSnakeFromInput)
}
async function runGame () {
var running = true
while (running) {
snake.move()
handleSnakeDeath(gameOver)
handleGoal()
paintGrid()
await delay()
}
function gameOver () {
alert('Game over!')
running = false
}
}
function handleSnakeDeath (callback) {
if (snake.isDead()) {
callback()
}
}
function handleGoal () {
const head = snake.getHead()
if (goal.isGoal(head[0], head[1])) {
snake.expand()
goal.newGoal()
}
}
function delay () {
return new Promise(resolve => {
setTimeout(resolve, 60)
})
}
CONTROLS
For the controls I have changed the "switch" to key mapping and object methods for better readability and modularity (you can now do moves.left() programmatically if you want).
// CONTROLS
const moves = {
left () {
snake.changeDirection(-1, 0)
},
up () {
snake.changeDirection(0, -1)
},
right () {
snake.changeDirection(0, 1)
},
down () {
snake.changeDirection(1, 0)
}
}
const keysMap = {
37: 'left',
38: 'up',
39: 'right',
40: 'down'
}
function moveSnakeFromInput (keyPress) {
if (keyPress in keysMap) {
moves[keysMap[keyPress]]()
}
}
RENDERING
For the rendering I have put the color variables together for easier changes. I have split updateDOM (which is now called paintGrid) into two methods : forEachCell and paintCell.
// RENDERING
const SNAKE_COLOR = '#00BFFF'
const GOAL_COLOR = 'yellow'
const BACKGROUND_1 = '#3dfc03'
const BACKGROUND_2 = '#03fc03'
function paintGrid () {
forEachCell(paintCell)
}
function forEachCell (iterator) {
for (let row = 0; row < HEIGHT; row++) {
for (let col = 0; col < WIDTH; col++) {
iterator(row, col)
}
}
}
function paintCell (row, col) {
if (snake.hasSnake(row, col)) {
paintSnake(row, col)
} else if (goal.isGoal(row, col)) {
paintGoal(row, col)
} else {
paintWall(row, col)
}
}
function paintSnake (row, col) {
paintTile(SNAKE_COLOR, row, col)
}
function paintWall (row, col) {
const WALL_COLOR = getBackgroundColor(row, col)
paintTile(WALL_COLOR, row, col)
}
function paintGoal (row, col) {
paintTile(GOAL_COLOR, row, col)
}
function paintTile (color, row, col) {
const tileDOM = getTile(row, col)
tileDOM.style.backgroundColor = color
}
function getTile (row, col) {
const gridDOM = document.querySelector('#grid')
const rowDOM = gridDOM.rows[row]
const tileDOM = rowDOM.cells[col]
return tileDOM
}
function initialiseDOMGrid () {
const gridDOM = document.querySelector('#grid')
for (let row = 0; row < HEIGHT; row++) {
let newRow = createDOMRow(row)
gridDOM.append(newRow)
}
paintGrid()
}
function createDOMRow () {
const newRow = document.createElement('tr')
newRow.className = 'row'
for (let col = 0; col < WIDTH; col++) {
let newTile = createDOMTile()
newRow.append(newTile)
}
return newRow
}
function createDOMTile () {
const newTile = document.createElement('td')
newTile.className = 'tile'
return newTile
}
function getBackgroundColor (row, col) {
return (row + col) % 2 === 0 ? BACKGROUND_1 : BACKGROUND_2
}
UTILS
Just some minor readability modifications here
// UTILS
function wrapAround ([row, col]) {
if (row === HEIGHT) {
row = 0
}
if (row < 0) {
row = HEIGHT - 1
}
if (col === WIDTH) {
col = 0
}
if (col < 0) {
col = WIDTH - 1
}
return [row, col]
}
function randomIntBetween (lower, upper) {
return Math.floor(Math.random() * (upper - lower)) + lower
}
Obviously this is just my opinion, so just pick what you like in my modifications :-)