Design a Snake game in Blazor

Introduction

Are you ready to embark on a nostalgic journey that takes you back to the days of the iconic Nokia 3310? Buckle up as we dive into the exciting world of game development and create a classic Snake game using Blazor, where the magic of modern web development meets the timeless fun of retro gaming.

The Game: A Quick Peek

As you step into the Snake Game arena, you'll immediately notice a sleek and modern game board. The dark theme sets the stage for the game. In my version of the game, our snake resembles a mighty dragon on a quest for some apples. Quite modern, I know.

Snake game

Snake game first look

The Game Board

The structure and aesthetics of our game board are defined in the Snake.razor file. This is where the action happens, where the snake slithers, and where apples await their fate.

Listing 1. Snake.razor

    <div class="game-container"
         tabindex="0"
         @onkeydown="ControlSnakeDirection">
        @for (int row = 0; row < NO_OF_ROWS; row++)
        {
            @for (int col = 0; col < N0_OF_COLS; col++)
            {
                bool isSnakeCell = IsSnakeCell(row, col);
                bool isSnakeHead = IsSnakeHead(row, col);
                bool isFoodCell = IsFoodCell(row, col);

                <div class="cell @(isSnakeCell && !isSnakeHead ? "snake-body" : "")">
                    @if (isSnakeCell)
                    {
                        @if (isSnakeHead)
                        {
                            <span>🐲</span>
                        }
                        else
                        {
                            <span>●</span>
                        }
                    }
                    @if (isFoodCell)
                    {
                        <span>🍎</span>
                    }
                </div>
            }
        }
    </div>

Explanation of Listing 1

On line 3, we're calling the "ControlSnakeDirection()" method every time a player presses an arrow key on the keyboard. This method controls the snake's movement in response to user input. But how do we determine which direction to move in?

1. We have the Direction enum, a neat way to represent the snake's movement options. 

public enum Direction
    {
        UP = 0,
        RIGHT = 1,
        DOWN = 2,
        LEFT = 3
    }
}

2. Next, on lines 4 and 6, we are using two nested loops to construct our game board, effectively dividing the page into a grid of 15x15 sections. The loop variables are populated from a static class GameHelper, which holds essential game-related information.

 public static class GameHelper
	{
		public const int NO_OF_ROWS = 15;
		public const int N0_OF_COLS = 15;
        public const int SNAKE_SPEED = 600;
    }

3. Our game comes alive with these three methods.

  1. IsSnakeCell(row, col): Checks if the current cell at the given row and column coordinates belongs to the snake's body. It returns a boolean value, revealing whether the cell is part of the snake's body.
  2. IsSnakeHead(row, col): Similar to the previous method, this one verifies whether the current cell represents the snake's head.
  3. IsFoodCell(row, col): This method detects whether the current cell contains the delicious apples 🍎.

4. Every cell on the board is assigned a purpose – it's either the Snake's head (🐲), the Snake's body (●), a tantalizing apple (🍎), or an empty cell.

The CSS classes we apply to each cell are more than just style – they're the visual cues that bring our game to life. If a cell belongs to the snake's body (but isn't the head), we give it the "snake-body" class, painting it with a green background. Meanwhile, the "cell" class is applied to every cell, resulting in those charming boxes that populate the board.

<div class="cell @(isSnakeCell && !isSnakeHead ? "snake-body" : "")">

4.1.  If "isSnakeHead" is true, then it displays either a dragon emoji (🐲) for the snake's head else a dot (●) for the snake's body.

@if(isSnakeCell)
{
    @if(isSnakeHead)
    {
        < span >🐲</ span >
    }
    else
    {
       < span >●</ span >
    }
}

4.2. If isFoodCell is true, it displays an apple emoji 🍎 to represent food.

@if (isFoodCell)
{
    <span>🍎</span>
}

Score Tracking

No game is complete without a way to track your progress. The "CurrentScore" and "TopScore" sit proudly at the top of the screen, providing you with real-time tracking.

To manage scores, we introduce the Score class with two essential fields.

  1. CurrentScore: This field updates throughout the game as you accumulate points.
  2. TopScore: The pinnacle of achievement, this field records your highest score ever achieved in the game.

Listing 2. class Score

public class Score
{
    public int CurrentScore;
    public int TopScore;
}

The Logic: Code Behind Blazor.razor.cs

Behind the scenes, in the Blazor.razor.cs file, lies the beating heart of our game – the logic that controls the snake's every move, manages the game's state and handles user input.

Fields and Properties

  • currentCell: This field represents the snake's current position on the game board, stored as an instance of a class SnakeCell with row and column coordinates.
  • snakeBody: A list that records the positions of every cell occupied by the snake's body.
  • score: An instance of the class Score introduced earlier, holding CurrentScore and TopScore.
  • isGameOver: A boolean flag to keep tabs on whether the game has concluded or is still in progress.
  • snakeDirection: An enum field (Direction) that retains the snake's current movement direction – be it UP, RIGHT, DOWN, or LEFT.
  • foodRow and foodCol: Integer fields that pinpoint the current location of the food on the game board.
public class SnakeCell
{
    public int Row { get; set; }

    public int Col { get; set; }
}

public partial class Snake
{
      SnakeCell currentCell;

      readonly List<SnakeCell> snakeBody = new();

      Score score = new();

      bool isGameOver;

      // Define the Snake's initial direction
      Direction snakeDirection = Direction.RIGHT;
     
      // Define the food's initial position
      int foodRow = 5;
      int foodCol = 5;
      
}

Initialization and Game Loop

OnInitializedAsync(): This method gets triggered when the component is first initialized. It sets the stage for our game by initializing essential parameters and initiating the game loop through the StartGame method.

protected override async Task OnInitializedAsync()
{
    InitializeGame();
    await StartGame();
}

InitializeGame(): The cradle of game parameters! This method births essential elements such as the snake's starting position, score, and, of course, the location of food.

private void InitializeGame()
{
    // Define the Snake's initial position
    currentCell = new() { Row = 10, Col = 10 };

    // Initialize the snake's size to 1
    score.CurrentScore = 1;

    // Initialize the snake's body with one cell at the starting position
    snakeBody.Add(CloneSnakeCell());

    // Generate the initial food
    GenerateFood();
}

StartGame(): An asynchronous powerhouse that constantly updates the game's state. It controls the snake's movement, checks for collisions, and tirelessly runs until the game is over.

private async Task StartGame()
{
    // Start the game loop
    while (!isGameOver)
    {
        UpdateSnakeDirection();
        if (IsFoodFound())
        {
            score.CurrentScore++;
            GenerateFood();
        }
        await Task.Delay(SNAKE_SPEED);
        StateHasChanged();
    }
}

Game Logic

ControlSnakeDirection(): A method to handle user input, ensuring that the snake changes direction appropriately based on arrow key presses. If you remember from listing 1, we are calling this method on the @KeyDown event.

private void ControlSnakeDirection(KeyboardEventArgs e)
{
    switch (e.Key)
    {
        case "ArrowUp":

            snakeDirection = Direction.UP;
            break;
        case "ArrowRight":

            snakeDirection = Direction.RIGHT;
            break;
        case "ArrowDown":

            snakeDirection = Direction.DOWN;
            break;
        case "ArrowLeft":

            snakeDirection = Direction.LEFT;
            break;
    }
}

UpdateSnakeDirection(): This method is the engine of our game, managing the snake's position updates according to its current direction. Then increase the size of the snake, and check if the game is over.

private void UpdateSnakeDirection()
{
    switch (snakeDirection)
    {
        case Direction.UP:
            currentCell.Row--;
            break;
        case Direction.RIGHT:
            currentCell.Col++;
            break;
        case Direction.DOWN:
            currentCell.Row++;
            break;
        case Direction.LEFT:
            currentCell.Col--;
            break;
    }

    // Add the new current Cell to the  of the snake's body
    snakeBody.Insert(0, CloneSnakeCell());

    //Check if Game is over
    IsGameOver();

    // Remove the last cell (tail) to maintain the snake's size
    if (snakeBody.Count > score.CurrentScore)
    {
        snakeBody.RemoveAt(snakeBody.Count - 1);
    }
}

IsGameOver(): The guardian of game-ending conditions! It watches for collisions with the game board's boundaries and offers players the choice to reset the game or explore my website https://rikampalkar.github.io.

private async Task IsGameOver()
{
    if (currentCell.Row < 0 || currentCell.Row >= NO_OF_ROWS || currentCell.Col < 0 || currentCell.Col >= N0_OF_COLS)
    {
        isGameOver = true;
        bool isResetGame = await js.InvokeAsync<bool>("ResetGamePopup", score.CurrentScore);
        if (isResetGame)
        {
            if (score.CurrentScore > score.TopScore)
            {
                score.TopScore = score.CurrentScore;
            }
            ResetGame();
        }
        else
        {
            await js.InvokeVoidAsync("navigateToWebsite", $"https://rikampalkar.github.io/");
        }
    }
    isGameOver = false;
}

ResetGame(): This method takes charge when players decide to start afresh, wiping the slate clean for a new game session.

private void ResetGame()
{
    snakeBody.Clear();
    isGameOver = false;
    OnInitializedAsync();
}

GenerateFood(): The chef behind the scenes! This function cooks up a new batch of food at random positions on the game board.

private void GenerateFood()
{
    var random = new Random();
    foodRow = random.Next(0, NO_OF_ROWS);
    foodCol = random.Next(0, N0_OF_COLS);
}

CloneSnakeCell(): This method clones the current cell's position, serving as a blueprint for snake body cells.

private SnakeCell CloneSnakeCell()
{
     return new SnakeCell() { Row = currentCell.Row, Col= currentCell.Col };
} 

CSS Methods

Our game wouldn't be complete without its visual flair, brought to you by these private methods. They determine how cells appear on the game board, ensuring that the snake body, snakehead, and food stand out just right.


//This method checks whether the cell at the given row and col coordinates belongs to the snake's body.
private bool IsSnakeCell(int row, int col)
{
    return snakeBody.Exists(cell => cell.Row == row && cell.Col == col);
}

//This method checks whether the cell at the given row and col coordinates matches the position of the food
private bool IsFoodCell(int row, int col)
{
     return row == foodRow && col == foodCol;
}

// Function to check if a cell is the snake head
private bool IsSnakeHead(int row, int col)
{
      return row == snakeBody[0].Row && col == snakeBody[0].Col;
}

// Check for collision between the Snake and food
private bool IsFoodFound()
{
      return currentCell.Row == foodRow && currentCell.Col == foodCol;
}

Source code

I have attached the source code for your convenience. It not only includes the Blazor implementation but also demonstrates how to use JavaScript to show alerts and provides the CSS styles used in this game. Feel free to download the source code and enjoy exploring its inner workings. You can find the source code at this github link and in the attachments of the article.

And for those hungry to delve deeper into the world of Blazor, I have exciting news! I've authored a comprehensive book titled "Blazor Simplified: A Guide to Essential Fundamentals", available on Amazon. This book is your passport to a more profound understanding of Blazor, offering insights, tips, and practical knowledge that will elevate your web development skills to new heights.

Conclusion

In this article, we embarked on a journey that seamlessly blended the retro charm of the Snake game with the cutting-edge capabilities of Blazor. We ventured into the intricacies of creating a classic game board, mastering user input, and effectively managing game states. It's more than just the end of an article; it's an invitation to embark on your own adventure. The skills you've learned in this mini-project extend far beyond recreating Snake Game. They serve as a versatile toolkit for innovation and problem-solving.

Keep coding, keep exploring, and keep pushing the boundaries of what you can achieve with your newfound Blazor skills. Your next remarkable project awaits!


Similar Articles