Hey everyone! Ever feel nostalgic for those simple, classic arcade games? I recently decided to revisit one of the greats, Snake! But instead of reaching for a hefty game engine or a complex frontend framework, I challenged myself to build it using plain, vanilla JavaScript. Why? For the fun of it, to brush up on core JS concepts, and to see how far we can get without extra tooling.
I also integrated Supabase to handle a persistent high score list. It's surprisingly easy to connect a backend-as-a-service like Supabase to a simple frontend project like this.
Repository: https://github.com/vianch/snake-js
Demo: Link
In a world full of React, Vue, Angular, Svelte, and countless other frameworks, why choose vanilla JavaScript?
Before diving into the code, let's look at how the project is organized:
The main index.html file, CSS for styling, and several JavaScript files, each with a specific responsibility. This separation makes the code easier to manage and understand.
src/index.html)The HTML is straightforward. It sets up:
snake.css) and the bundled JavaScript (snake-bundle.min.js - though we'll focus on the source files). It also imports the retro "VT323" font from Google Fonts.div elements (id="score", id="high-score") to show the current and top scores.class="game-container") holding the game board (id="game-board") and initial instructions (id="instructions-container"). Nested divs are used to create the visual border effect.id="arrow-up", etc.) and a start button (id="start-button) for mobile or users who prefer clicking.id="modal") that will pop up to display the list of recent high scores fetched from Supabase.The snake.css file (which we won't dive deep into here) uses basic CSS properties along with CSS Grid to lay out the game board and position the snake and food elements. The "VT323" font gives it that classic, pixelated look.
src/assets/js/index.js, constants.js, utils.js)index.js: This is the starting point. It's incredibly simple: it imports the main SnakeGame class and creates a new instance. Then, it calls the listener() method on the instance to set up all the necessary event listeners.
constants.js: This file is crucial for maintainability. Instead of scattering values like key codes, directions, database table names, or game speed throughout the code, they are defined here as exported constants. This makes it easy to change them later if needed. It also holds the Supabase URL and public API key (anon key).
utils.js: Contains simple helper functions used elsewhere. generateRandomNumber is used for placing food, and formatDate makes the timestamps from Supabase more readable in the score list.
src/assets/js/board.js)The Board class acts as an interface between the game logic and the HTML document (the DOM). It doesn't know how the snake moves, only how to display things on the screen.
document.getElementById.appendElementToBoard(element) / appendElementsToBoard(elements): Adds game elements (like snake segments or food) to the board div.clearBoard(): Removes all child elements from the board div, effectively clearing it for the next frame.startBoard() / endBoard(): Manages the visibility of the instructions vs. the game board itself. endBoard also handles the blinking "death" animation.populateScores(scores): Takes an array of score data (from Supabase) and dynamically creates HTML elements to display them in the score table modal.showScoreModal() / hideScoreModal(): Controls the visibility of the score list modal.createGameElement(elementType, className): A helper to create a DOM element (like a div) and add a CSS class to it (e.g., 'snake' or 'food').setPosition(element, segment): Takes a DOM element and a position object ({x, y}) and sets the element's gridRowStart and gridColumnStart CSS properties to place it correctly on the CSS Grid.createScoreEntry(scoreData, index): Creates the HTML structure for a single row in the high score table.src/assets/js/snake.js)This is where the magic happens! The SnakeGame class extends the Board class, inheriting all its DOM manipulation capabilities, and adds the specific rules and state for Snake.
Initialization & State (constructor):
snakeDirection, previousSnakeDirection, snakePositions (starts with one segment), foodPosition (randomly generated), gameStarted (false).#gridSize, #gameSpeedDelay.Database class to interact with Supabase.#loadHighScores and #loadScores immediately to fetch existing data from Supabase.The Game Loop & Drawing:
#initializeGameInterval(): Uses setInterval to create the main game loop. The interval is determined by #gameSpeedDelay.#moveSnake(), #checkCollision(), and draw() are called repeatedly.draw(): If the game has started, it calls clearBoard(), then #drawSnake() and #drawFood().#drawSnake(): Maps over the snakePositions array. For each segment, it creates a 'snake' element using Board.createGameElement and positions it using Board.setPosition.#drawFood(): Creates and positions the 'food' element.Movement & Input (#moveSnake, #handleKeyPress, listener):
listener(): Sets up event listeners for keyboard presses (keydown) and clicks on the on-screen controls. All inputs trigger #handleKeyPress.#handleKeyPress(event):
#isPressingKey prevents rapid, conflicting inputs).Space), ending (Escape), and closing the modal (Escape).snakeDirection but includes logic to prevent the snake from immediately reversing (e.g., can't go up if currently moving down). It checks both the current and previous direction to handle quick turns smoothly.#moveSnake():
snakeDirection.unshifts the new head position onto the snakePositions array (adds to the beginning).#eatFood() to check if food was eaten.Collision Detection (#checkCollision):
snakeHeadPosition coordinates are outside the grid boundaries (1 to #gridSize).snakeHeadPosition matches the coordinates of any other segment in the snakePositions array (using slice(1) to exclude the head itself).endGame().Eating & Growing (#eatFood, #generateFoodPosition):
#eatFood():
snakeHeadPosition matches the foodPosition.#increaseSnakeSpeed().#updateScore().foodPosition using #generateFoodPosition().#initializeGameInterval() again to restart the loop with the potentially new speed.snakePositions.pop(). This is how the snake appears to move – add a new head, remove the tail.#generateFoodPosition(): Calculates random X/Y coordinates within the grid. It recursively calls itself if the generated position is already occupied by a snake segment.Difficulty Scaling (#increaseSnakeSpeed):
#gameSpeedDelay slightly, making the setInterval run faster.Game State & Score Management (startGame, resetGame, endGame, #updateScore, #updateHighScore, #saveHighScore):
startGame(): Sets gameStarted to true, updates the board display, and starts the game loop interval.resetGame(): Clears the interval, sets gameStarted to false, resets the board display.endGame(): Calls resetGame, updates/saves the high score via #updateHighScore, resets snake position, direction, and speed, and updates the score display.#updateScore(): Calculates score (snakePositions.length - 1) and updates the score display element.#updateHighScore(): Compares the current score to #highScore. If higher, updates #highScore locally, updates the display, and calls #saveHighScore() to persist it to Supabase. It always saves the current score (even if not a high score) to the scores table using this.#database.insertData.#saveHighScore(): Calls this.#database.updateData to update the single high score record in the highScores table.src/assets/js/database.js)This is where Supabase comes in. The Database class encapsulates all interaction with the backend.
Setting up the Connection (constructor):
anon key (from constants.js).createClient from the @supabase/supabase-js library to create a Supabase client instance.Fetching and Saving Scores:
fetchData(tableName, columnName, options):
from(tableName).select(columnName) method.options object.updateData(tableName, newValue):
from(tableName).update([newValue]).eq("id", 1). This is specifically tailored to update the single row in the highScores table (assuming its id is 1).insertData(tableName, newValue):
from(tableName).insert([newValue]). Used to add the result of every game to the scores table.By encapsulating the Supabase logic here, the rest of the game code (snake.js) doesn't need to know the specifics of the Supabase API – it just calls methods like this.#database.fetchData(...).
index.html loads.index.js runs, creates a SnakeGame instance, and calls listener().SnakeGame constructor initializes state, creates the Database instance, and fetches initial scores/high score from Supabase via the Database instance.listener and #handleKeyPress).startGame() is called: the board is shown, gameStarted becomes true, and #initializeGameInterval() starts the loop.#moveSnake() calculates the new head position and adds it.#eatFood() checks for food; if not eaten, it removes the tail segment. If eaten, it updates score/speed and generates new food.#checkCollision() checks for game-ending conditions.draw() clears the board (Board.clearBoard) and redraws the snake and food in their new positions (Board.createGameElement, Board.setPosition).#checkCollision() detects a hit and calls endGame().endGame() stops the loop, updates the display, calls #updateHighScore().#updateHighScore() saves the score to the scores table and potentially updates the highScores table in Supabase via the Database instance methods.Board.showScoreModal and SnakeGame.#loadScores (which calls Database.fetchData) to display the score list.Building this Snake game was a fun exercise in using vanilla JavaScript for game development. It reinforces how much you can achieve with just the browser's built-in tools. Integrating Supabase was surprisingly straightforward and added a valuable feature – persistent high scores – without needing to build or manage a custom backend.
If you want to extend this, you could:
A classic Snake game implemented in JavaScript, providing a nostalgic trip with a modern twist.
Movement: Use the arrow keys ←, ↑, ↓, → to control the snake's direction. For mobile or touch devices, use the on-screen arrows.
Start Game: Press the spacebar or use the Start button to begin the game.
View Scores: Click on "See scores" to check the latest 10 scores displayed on the top right corner of the game screen.
Scores: High scores and the current score are displayed on the top left corner during gameplay.
I hope this walkthrough was helpful! It shows that you don't always need complex frameworks for fun projects. Sometimes, getting back to basics is the best way to learn and create. Happy coding!