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.
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.
Previous Posts in this Series
You may want to read these first.
Cells
The smallest unit we can break our Tetris model into is a cell, or a single spot on the game grid.
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.
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:
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!