Building a Snake Game App with Next.js

Day 26: Snake Game — 30 Days of 30 Projects Challenge

Asharib Ali
9 min readOct 3, 2024

Hey everyone, I hope you’re all doing well and enjoying your coding journey, Am I right? By the way, today marks the 26th day of the 30-day of 30-projects challenge. Our 26th project will be creating a mini Next.js application — a snake game. Please make sure to clap and comment on this blog post. It motivates me to create more amazing content like this, Let’s get started straight away.

Overview of the Mini Next.js Application

Our Snake Game application allows users to:

  • Start, stop, and pause the game
  • Interactive gaming theme
  • Display the final results, and try again

Tech-Stack Used:

  • Next.js: A React framework for building full-stack web applications.
  • React: A JavaScript library for building user interfaces.
  • Tailwind CSS: A utility-first CSS framework for styling.
  • Shadcn UI: Beautifully designed tailwindcss components that you can copy and paste into your application.
  • Vercel: For deploying the Nextjs web application.

Initialize a Nextjs project

Start by following this guide to set up the new project.

  • Go to the “components” folder.
  • Create a new file named “snake-game.tsx”.
  • This file will manage the entire functionality of the project.

We will go through the code step-by-step to make it easy to understand.

Component Breakdown

Import Statements

"use client";
import { useState, useEffect, useCallback, useRef } from "react";
import { Button } from "@/components/ui/button";
import { PauseIcon, PlayIcon, RefreshCcwIcon } from "lucide-react";
  • Enables client-side rendering.
  • Imports necessary hooks and custom components for building the UI.

Define Game States and Directions

enum GameState {
START,
PAUSE,
RUNNING,
GAME_OVER,
}


enum Direction {
UP,
DOWN,
LEFT,
RIGHT,
}

interface Position {
x: number;
y: number;
}

const initialSnake: Position[] = [{ x: 0, y: 0 }];
const initialFood: Position = { x: 5, y: 5 };
  • Defines the game states and directions for the snake movement.
  • Defines the Position interface and initial state for the snake and food.

Component Definition and Initial State

export default function SnakeGameComponent() {
const [gameState, setGameState] = useState<GameState>(GameState.START);
const [snake, setSnake] = useState<Position[]>(initialSnake);
const [food, setFood] = useState<Position>(initialFood);
const [direction, setDirection] = useState<Direction>(Direction.RIGHT);
const [score, setScore] = useState<number>(0);
const [highScore, setHighScore] = useState<number>(0);
const gameInterval = useRef<NodeJS.Timeout | null>(null);
  • Defines the SnakeGameComponent function.
  • Manages state for the game, snake, food, direction, score, and high score.

Move Snake Function

const moveSnake = useCallback(() => {
setSnake((prevSnake) => {
const newSnake = [...prevSnake];
const head = newSnake[0];
let newHead: Position;

switch (direction) {
case Direction.UP:
newHead = { x: head.x, y: head.y - 1 };
break;
case Direction.DOWN:
newHead = { x: head.x, y: head.y + 1 };
break;
case Direction.LEFT:
newHead = { x: head.x - 1, y: head.y };
break;
case Direction.RIGHT:
newHead = { x: head.x + 1, y: head.y };
break;
default:
return newSnake;
}
newSnake.unshift(newHead);
if (newHead.x === food.x && newHead.y === food.y) {
setFood({
x: Math.floor(Math.random() * 10),
y: Math.floor(Math.random() * 10),
});
setScore((prevScore) => prevScore + 1);
} else {
newSnake.pop();
}
return newSnake;
});
}, [direction, food]);
  • Moves the snake in the current direction.
  • Increases the snake’s length when it eats the food and spawns new food at a random position.

Handle Key Press Events

const handleKeyPress = useCallback(
(event: KeyboardEvent) => {
switch (event.key) {
case "ArrowUp":
if (direction !== Direction.DOWN) setDirection(Direction.UP);
break;
case "ArrowDown":
if (direction !== Direction.UP) setDirection(Direction.DOWN);
break;
case "ArrowLeft":
if (direction !== Direction.RIGHT) setDirection(Direction.LEFT);
break;
case "ArrowRight":
if (direction !== Direction.LEFT) setDirection(Direction.RIGHT);
break;
}
},
[direction]
);
  • Handles key press events to change the snake’s direction.

useEffect for Game Interval and Key Press Events

useEffect(() => {
if (gameState === GameState.RUNNING) {
gameInterval.current = setInterval(moveSnake, 200);
document.addEventListener("keydown", handleKeyPress);
} else {
if (gameInterval.current) clearInterval(gameInterval.current);
document.removeEventListener("keydown", handleKeyPress);
}

return () => {
if (gameInterval.current) clearInterval(gameInterval.current);
document.removeEventListener("keydown", handleKeyPress);
};
}, [gameState, moveSnake, handleKeyPress]);
  • Manages the game interval and key press events based on the game state.

Start, Pause, Reset Game Functions

const startGame = () => {
setSnake(initialSnake);
setFood(initialFood);
setScore(0);
setDirection(Direction.RIGHT);
setGameState(GameState.RUNNING);
};

const pauseGame = () => {
setGameState(
gameState === GameState.RUNNING ? GameState.PAUSE : GameState.RUNNING
);
};


const resetGame = () => {
setGameState(GameState.START);
setSnake(initialSnake);
setFood(initialFood);
setScore(0);
};
  • Functions to start, pause, and reset the game.

Update High Score

useEffect(() => {
if (score > highScore) {
setHighScore(score);
}
}, [score, highScore]);
  • Updates the high score if the current score exceeds the high score.

Render Snake Game UI

return (
<div className="flex flex-col items-center justify-center h-screen bg-gradient-to-br from-[#0F0F0F] to-[#1E1E1E]">
<div className="bg-[#1E1E1E] rounded-lg shadow-lg p-8 w-full max-w-md">
<div className="flex items-center justify-between mb-6">
<div className="text-3xl font-bold text-[#FF00FF]">Snake Game</div>
<div className="flex gap-4">
<Button
variant="ghost"
size="icon"
className="text-[#00FFFF]"
onClick={startGame}
>
<PlayIcon className="w-6 h-6" />
<span className="sr-only">Start</span>
</Button>
<Button
variant="ghost"
size="icon"
className="text-[#00FFFF]"
onClick={pauseGame}
>
<PauseIcon className="w-6 h-6" />
<span className="sr-only">Pause/Resume</span>
</Button>
<Button
variant="ghost"
size="icon"
className="text-[#00FFFF]"
onClick={resetGame}
>
<RefreshCcwIcon className="w-6 h-6" />
<span className="sr-only">Reset</span>
</Button>
</div>
</div>
<div className="bg-[#0F0F0F] rounded-lg p-4 grid grid-cols-10 gap-1">
{Array.from({ length: 100 }).map((_, i) => {
const x = i % 10;
const y = Math.floor(i / 10);
const isSnakePart = snake.some(
(part) => part.x === x && part.y === y
);
const isFood = food.x === x && food.y === y;
return (
<div
key={i}
className={`w-5 h-5 rounded-sm ${
isSnakePart
? "bg-[#FF00FF]"
: isFood
? "bg-[#00FFFF]"
: "bg-[#1E1E1E]"
}`}
/>
);
})}
</div>
<div className="flex items-center justify-between mt-6 text-[#00FFFF]">
<div>Score: {score}</div>
<div>High Score: {highScore}</div>
</div>
</div>
</div>
);
}
  • Renders the Snake Game UI, including the game board, control buttons, and score display.

(Bonus just for you): Full Code with Comments

"use client"; // Enables client-side rendering for this component

import { useState, useEffect, useCallback, useRef } from "react"; // Import React hooks
import { Button } from "@/components/ui/button"; // Import custom Button component
import { PauseIcon, PlayIcon, RefreshCcwIcon } from "lucide-react"; // Import icons from lucide-react

// Define the possible game states
enum GameState {
START,
PAUSE,
RUNNING,
GAME_OVER,
}

// Define the directions for the snake movement
enum Direction {
UP,
DOWN,
LEFT,
RIGHT,
}

// Define the Position interface
interface Position {
x: number;
y: number;
}

// Initial state for the snake and food
const initialSnake: Position[] = [{ x: 0, y: 0 }];
const initialFood: Position = { x: 5, y: 5 };

export default function SnakeGame() {
// State to manage the game
const [gameState, setGameState] = useState<GameState>(GameState.START);
const [snake, setSnake] = useState<Position[]>(initialSnake);
const [food, setFood] = useState<Position>(initialFood);
const [direction, setDirection] = useState<Direction>(Direction.RIGHT);
const [score, setScore] = useState<number>(0);
const [highScore, setHighScore] = useState<number>(0);
const gameInterval = useRef<NodeJS.Timeout | null>(null);

// Function to move the snake
const moveSnake = useCallback(() => {
setSnake((prevSnake) => {
const newSnake = [...prevSnake];
const head = newSnake[0];
let newHead: Position;

switch (direction) {
case Direction.UP:
newHead = { x: head.x, y: head.y - 1 };
break;
case Direction.DOWN:
newHead = { x: head.x, y: head.y + 1 };
break;
case Direction.LEFT:
newHead = { x: head.x - 1, y: head.y };
break;
case Direction.RIGHT:
newHead = { x: head.x + 1, y: head.y };
break;
default:
return newSnake;
}

newSnake.unshift(newHead);

if (newHead.x === food.x && newHead.y === food.y) {
// Snake eats the food
setFood({
x: Math.floor(Math.random() * 10),
y: Math.floor(Math.random() * 10),
});
setScore((prevScore) => prevScore + 1);
} else {
newSnake.pop(); // Remove the last part of the snake's body
}

return newSnake;
});
}, [direction, food]);

// Function to handle key press events
const handleKeyPress = useCallback(
(event: KeyboardEvent) => {
switch (event.key) {
case "ArrowUp":
if (direction !== Direction.DOWN) setDirection(Direction.UP);
break;
case "ArrowDown":
if (direction !== Direction.UP) setDirection(Direction.DOWN);
break;
case "ArrowLeft":
if (direction !== Direction.RIGHT) setDirection(Direction.LEFT);
break;
case "ArrowRight":
if (direction !== Direction.LEFT) setDirection(Direction.RIGHT);
break;
}
},
[direction]
);

// useEffect to handle the game interval and key press events
useEffect(() => {
if (gameState === GameState.RUNNING) {
gameInterval.current = setInterval(moveSnake, 200);
document.addEventListener("keydown", handleKeyPress);
} else {
if (gameInterval.current) clearInterval(gameInterval.current);
document.removeEventListener("keydown", handleKeyPress);
}

return () => {
if (gameInterval.current) clearInterval(gameInterval.current);
document.removeEventListener("keydown", handleKeyPress);
};
}, [gameState, moveSnake, handleKeyPress]);

// Function to start the game
const startGame = () => {
setSnake(initialSnake);
setFood(initialFood);
setScore(0);
setDirection(Direction.RIGHT);
setGameState(GameState.RUNNING);
};

// Function to pause or resume the game
const pauseGame = () => {
setGameState(
gameState === GameState.RUNNING ? GameState.PAUSE : GameState.RUNNING
);
};

// Function to reset the game
const resetGame = () => {
setGameState(GameState.START);
setSnake(initialSnake);
setFood(initialFood);
setScore(0);
};

// useEffect to update the high score
useEffect(() => {
if (score > highScore) {
setHighScore(score);
}
}, [score, highScore]);

// JSX return statement rendering the Snake Game UI
return (
<div className="flex flex-col items-center justify-center h-screen bg-gradient-to-br from-[#0F0F0F] to-[#1E1E1E]">
<div className="bg-[#1E1E1E] rounded-lg shadow-lg p-8 w-full max-w-md">
<div className="flex items-center justify-between mb-6">
<div className="text-3xl font-bold text-[#FF00FF]">Snake Game</div>
<div className="flex gap-4">
<Button
variant="ghost"
size="icon"
className="text-[#00FFFF]"
onClick={startGame}
>
<PlayIcon className="w-6 h-6" />
<span className="sr-only">Start</span>
</Button>
<Button
variant="ghost"
size="icon"
className="text-[#00FFFF]"
onClick={pauseGame}
>
<PauseIcon className="w-6 h-6" />
<span className="sr-only">Pause/Resume</span>
</Button>
<Button
variant="ghost"
size="icon"
className="text-[#00FFFF]"
onClick={resetGame}
>
<RefreshCcwIcon className="w-6 h-6" />
<span className="sr-only">Reset</span>
</Button>
</div>
</div>
<div className="bg-[#0F0F0F] rounded-lg p-4 grid grid-cols-10 gap-1">
{Array.from({ length: 100 }).map((_, i) => {
const x = i % 10;
const y = Math.floor(i / 10);
const isSnakePart = snake.some(
(part) => part.x === x && part.y === y
);
const isFood = food.x === x && food.y === y;
return (
<div
key={i}
className={`w-5 h-5 rounded-sm ${
isSnakePart
? "bg-[#FF00FF]"
: isFood
? "bg-[#00FFFF]"
: "bg-[#1E1E1E]"
}`}
/>
);
})}
</div>
<div className="flex items-center justify-between mt-6 text-[#00FFFF]">
<div>Score: {score}</div>
<div>High Score: {highScore}</div>
</div>
</div>
</div>
);
}

Okay, you’ve completed the main component with functional UI. Now, you need to import this component into the app directory to use it in the app/page.tsx file. Your final code should look like this:

import SnakeGame from "@/components/snake-game";

export default function Home() {
return (
<div>
<SnakeGame />
</div>
);
}

Running the Project

To see the snake game in action, follow these steps:

  1. Start the Development Server: Run npm run dev to start the development server.
  2. Open in Browser: Open http://localhost:3000 in your browser to view the application.

Make sure to test it properly (each and everything) so that we don’t have any errors in the production mode aka when we host on the internet.

Now, we want people to see our application on the internet. All you have to do is create a repository on GitHub and then push your code to it. After that, deploy the Snake Game application using Vercel.

Once you’re done with the deployment, please share the application link with me by commenting on this blog post, on Linkedin, and (most importantly, on X a.k.a. Twitter. Tag me there, and I’ll reply and appreciate your efforts) 👀

(Optional): One thing you can do on your own is to add new functionalities, enhance the styling, and improve the overall application. This way, you’ll learn something new by making modifications.

✨ Star Github Repository of the project 👈

Conclusion

In this blog post, we built a Snake Game application using Next.js. We covered:

  • Setting up the project and using client-side rendering.
  • Managing state and game logic in a React application.
  • Implementing user input handling for game control.

See you tomorrow with the latest project. Happy coding!

Stay updated with the latest in cutting-edge technology! Follow me:

Thanks for reading!

--

--

Asharib Ali
Asharib Ali

Written by Asharib Ali

✨ I build & teach about AI and Blockchain stuffs⚡

Responses (1)