With our logical model we worked out in the first part of this series, we can begin to construct the C# model for our Tetris demo. Let's begin coding up our Tetris in Blazor demo by creating classes for the cells, the grid, and the game state.

This is part of our ultimate goal. We've still got a ways to go. Don't give up!

The Sample Project

Don't forget to check out the entire BlazorGames repository over on GitHub! All the code used in this series is there.

exceptionnotfound/BlazorGames
Contribute to exceptionnotfound/BlazorGames development by creating an account on GitHub.

Cells

The smallest unit we can break our Tetris model into is a cell, or a single spot on the game grid.

Each of these little blocks is an individual cell

At the most basic, a cell is either filled or not filled, occupied or not occupied. A cell must also have a position, or coordinate, in the larger grid (so that no two cells have the same coordinates).

There's one small trick we must also account for: since cells can be filled by different colors (each tetromino is a different color) each cell must also have its own CSS class.

The easiest way to see that each cell needs their own CSS class is to look at the bottom-most row.

Our Cell C# class looks like this:

public class Cell
{
    public int Row { get; set; }
    public int Column { get; set; }
    public string CssClass { get; set; }

    public Cell(int row, int column)
    {
        Row = row;
        Column = column;
    }

    public Cell(int row, int column, string css)
    {
        Row = row;
        Column = column;
        CssClass = css;
    }
}

Cells individually are not terribly interesting. As part of the larger collection, though, they represent a sizable portion of our total functionality.

The Cell Collection

We know from our theoretical model in Part 1 of this series that we will need some kind of collection for the cells in the game grid, and it cannot be a mere List<T>. Let's create a very basic collection class CellCollection:

public class CellCollection
{
    private readonly List<Cell> _cells = new List<Cell>();
}

Assumptions

We're going to make an assumption about this class: it will be used in BOTH the Grid and the individual tetrominos. In the former, it will contain all the filled cells; in the latter, it will represent all the cells in the grid that are currently occupied by said tetromino. Therefore, the functionality of CellCollection needs to cater to each of these objects.

Further, we are going to implement our CellCollection so that it only contains filled cells; cells which are not currently filled will not exist in an instance of CellCollection.

Functionality

Let's consider the kinds of things that may happen to a collection of cells. There are several different functionalities we need to consider, so we will handle them one at a time.

Basic Functionality

For several different reasons, we will need to know the following:

  • If there are any occupied cells in a given row or column.
  • If a specific cell is occupied.

So we need methods for each of those situations:

public class CellCollection
{
    //...Other properties and methods

    //Checks if there are any occupied cells in the given row
    public bool HasRow(int row)
    {
        return _cells.Any(c => c.Row == row);
    }
    
    //Checks if there are any occupied celld in the given column
    public bool HasColumn(int column)
    {
        return _cells.Any(c => c.Column == column);
    }
    
    //Checks if there is an occupied cell at the given coordinates.
    public bool Contains(int row, int column)
    {
        return _cells.Any(c => c.Row == row && c.Column == column);
    }
}

We'll show more usage of these methods in Part 3 of this series.

Tetromino Instantiation

When a tetromino is instantiated, we need to add cells to the collection that represent the initial occupied cells for that tetromino.

public class CellCollection
{
    //...Other properties and methods

    //Add a new cell to the collection
    public void Add(int row, int column)
    {
        _cells.Add(new Cell(row, column));
    }
}

"Stuck" Tetrominos

In the Grid's cell collection, we must be able to add occupied cells to the Grid's collection when pieces become "stuck" (that is, they can no longer move). At this point, the occupied cells are not part of a tetromino, and therefore must have their own CSS classes:

public class CellCollection
{
    //...Other properties and methods

    //Adds several new cells, each with the given CSS class
    public void AddMany(List<Cell> cells, string cssClass)
    {
        foreach(var cell in cells)
        {
            _cells.Add(new Cell(cell.Row, cell.Column, cssClass));
        }
    }
}

In order to do that, though, we should have a way to get all cells in the current tetromino's collection:

public class CellCollection
{
    //...Other properties and methods

    //Returns all occupied cells
    public List<Cell> GetAll()
    {
        return _cells;
    }
}

Clearing Rows

When a row is complete, we will do two things. First, a we will add a CSS class to every cell in the completed row that will cause the cells to flash briefly.

public class CellCollection
{
    //...Other properties and methods

    //Adds a CSS class to every cell in a given row
    public void SetCssClass(int row, string cssClass)
    {
        _cells.Where(x => x.Row == row)
              .ToList()
              .ForEach(x => x.CssClass = cssClass);
    }
}

Second, when the animation is done, we will remove the completed row from the collection.

public class CellCollection
{
    //...Other properties and methods

    //Moves all "higher" cells down to fill in the specified completed rows.
    public void CollapseRows(List<int> rows)
    {
        //Get all cells in the completed rows
        var selectedCells = _cells.Where(x => rows.Contains(x.Row));
        
        //Add those cells to a temporary collection
        List<Cell> toRemove = new List<Cell>();
        foreach (var cell in selectedCells)
        {
            toRemove.Add(cell);
        }

        //Remove all cells in the temporary collection 
        //from the real collection.
        _cells.RemoveAll(x => toRemove.Contains(x));

        //"Collapse" the rows above the complete rows by moving them down.
        foreach (var cell in _cells)
        {
            int numberOfLessRows = rows.Where(x => x <= cell.Row).Count();
            cell.Row -= numberOfLessRows;
        }
    }
}

Leftmost and Rightmost

In Part 3 of this series, we will need a way to get the "leftmost" and "rightmost" cells in a collection, so that we can check if a tetromino is able to move left or right. This check needs to be part of CellCollection, so we're going to include it now.

public class CellCollection
{
    //...Other properties and methods
    
    // Gets the rightmost (highest Column value) cell in the collection.
    public List<Cell> GetRightmost()
    {
        List<Cell> cells = new List<Cell>();
        foreach (var cell in _cells)
        {
            if (!Contains(cell.Row, cell.Column + 1))
            {
                cells.Add(cell);
            }
        }

        return cells;
    }

    // Gets the leftmost (lowest Column value) cell in the collection.
    public List<Cell> GetLeftmost()
    {
        List<Cell> cells = new List<Cell>();
        foreach (var cell in _cells)
        {
            if (!Contains(cell.Row, cell.Column - 1))
            {
                cells.Add(cell);
            }
        }

        return cells;
    }
}

Lowest

We also need a method to get the lowest cell, e.g. the cell closest to the bottom of the grid. This is so we can check if a tetromino can move down.

public class CellCollection
{
    //...Other properties and methods
    
    // Gets the lowest (lowest Row value) cell in the collection. 
    public List<Cell> GetLowest()
    {
        List<Cell> cells = new List<Cell>();
        foreach(var cell in _cells)
        {
            if(!Contains(cell.Row - 1, cell.Column))
            {
                cells.Add(cell);
            }
        }

        return cells;
    }
}

Note that the GetLeftmost(), GetRightmost(), and GetLowest() methods use the Contains() method we defined earlier.

OK! That's the complete implementation for the CellCollection class! You can see the entire class over on GitHub:

exceptionnotfound/BlazorGames
Contribute to exceptionnotfound/BlazorGames development by creating an account on GitHub.

Game State

As with many of the previous projects in BlazorGames, we will have an enumeration for our Tetris implementation that represents the current state of the game.

Surprisingly, considering how complicated our Blackjack game's version of GameState was, the version for Tetris only has three states.

  • When a game is not yet started.
  • When a game is in progress.
  • When the game is over.

Hence, we have our GameState enum:

public enum GameState
{
    NotStarted, 
    Playing,
    GameOver
}

Summary

In this post, we've starting creating the building blocks (pun intended) of our Tetris in Blazor implementation: the Cell class, the CellCollection, and the GameState enum.

In the next part of this series, we'll implement the tetrominos as C# classes and show how to write methods for their movement and rotation. Stick around!

Happy Coding!


Hello Dear Reader! Want to get the best C#, ASP.NET, web tech, tips, and stories anywhere on the Web? Sign up to receive my premium newsletter The Catch Block! Each Wednesday, you'll get the best reads, job listings, stories, tips, and news from around the ASP.NET and C# worlds. All for only $5/month or $50/year! Become a subscriber today!