Polymorphic Console Tetris

July 7, 2020

For a bit of personal development and after first playing on an original Game Boy in the mid 1990s, I decided to attempt recreating the game Tetris in C++.

I follow what I think is an excellent C++ programming channel on Youtube by a man under the name “Javidx9”. He mainly specializes in the application of C++ to game design and high performance computing which are of course two areas where C++ shines, but also discusses other topics such as robotics and image processing.

I was particularly inspired by his video on Code-It-Yourself Tetris. In it, he utilized the windows console and simple text to serve as his game’s display. As someone with a casual interest in game development, it seemed like a great project to keep me busy during this year’s down-time. 

In an effort to keep the video relatively short (It’s still over 30 minutes long), he kept the actual programming simple while using a bulk of this time explaining concepts like his indexing methods and matrix rotations. My own implementation takes the same concepts he presented, but applies them differently to create a more extensible and abstract solution.

My main goals for this project were to:

  • Create an extensible design that can be easily ported/added on to
  • Decouple the game’s logic from its input and output
  • Utilize the Windows console as a graphical display

Extensiblity

This project makes what I feel is good use of polymorphism to achieve a design that is easily extensible. For example, creating a “Tetris” object involves passing in 2 interface arguments whose parameters must implement input/output for the program. In this implementation, those are the keyboard and console, but the interface is sufficiently abstracted to easily configure control with a game pad or display it with a more sophisticated graphics API.

int main()
{
	ConsolePlayingField outputWindow(STANDARD_WIDTH, STANDARD_HEIGHT);
	KeyboardInput input;
	Tetris tetris(input, outputWindow, customDelayFunc);

	tetris.Run(); 

	return 0; 
}

Decoupling

In order to decouple the game from its display implementation, the gameplay data itself is reduced to an array of Width * Height characters that the Tetris object passes into an object that implements its IPlayingField interface class argument. This along with data of how many lines have been cleared is the only information that leaves the object.

Console Graphics

Using the Windows console to display gameplay data seemed like the easiest route to start with. Plus, since I accomplished my first 2 goals, extending it to something more sophisticated is straightforward. 

Description

To achieve a more architected game, I based my code off of some of the ideas from another Github repository by a user that recreated the entire first level of Super Mario Brothers on the NES in python. In it, the game was controlled mainly by a control object that handled moving between different game “states”. States being individual units such as the main menu, loading screen, and the entirety of the first level. I wanted to keep this project as more of an exercise in decoupling so I haven’t created more than 1 game state outside of the gameplay itself at the time of this writing, but the option is open.

In my implementation, my Tetris object serves as the master control object that passes inputs into my gameplay state and passes the subsequent gameplay data to an output buffer which in my case is a Console playing field.

void Tetris::Run()
{
  while (true)
  {
    updateTime();
    updateInputs();
    updateState();
    updateDisplay();
  }
}

Once in my Console object, if formats the data from raw text into something more colorful and interesting to look at.

Normal gamplay on the left and raw text input with no colors applied to piece data on the right

The gameplay itself is mainly manipulation of arrays. Because my Tetrominos are defined as a constant single dimension array, indexing into them is a bit less straightforward than if they were two dimensional. The math however is easy once you understand how they translate.

Gameplay::Piece Gameplay::rotatePiece(Piece piece)
{
	Gameplay::Piece rotatedPiece{ {0}, piece.displayCharacter, 
                                     piece.x_pos, piece.y_pos};
	uint32_t index = 0;

	for (int i = 0; i < piece.shape.size(); i++) {
		index = (sideLength * 3) + yIndexOffset(i) - 
                        (xIndexOffset(i) * 4);
		rotatedPiece.shape[i] = piece.shape[index];
	}

	return rotatedPiece;
}

In this function, I have a loop that checks every element of my piece’s array. I also have an index variable that changes based on a formula for calculating what the resulting array’s index would be if it was turned 90 degrees to the right. Javidx9’s video on his Tetris implementation derives this nicely.

Screenshot of matrix rotation derivation and resulting 90 degree formula from Javidx9’s video

Since I’m not working with 2 dimensional arrays, my formula looks a bit different. In mine, I’ve subbed in the X and Y values for offset functions that allow me to translate the function from 2D to 1D

uint8_t Gameplay::xIndexOffset(uint8_t index) {
	return index % sideLength;
}

This simply returns a modulus operation of the argument. Since this operation bounds any value passed in to the size of the variable on the right of the operator which in my case is of size 4, it serves as an index into my X value.

uint8_t Gameplay::yIndexOffset(uint8_t index) {
	if (index >= 0 && index < sideLength) return 0;
	if (index >= sideLength && index < sideLength * 2) return 1;
	if (index >= sideLength * 2 && index < sideLength * 3) return 2;
	if (index >= sideLength * 3 && index < sideLength * 4) return 3;

	return 0;
}

While I’m not necessarily happy with this since I’d have to add to it to handle larger side lengths, it does work. For Y, I needed to bound my index as well to my side length, but my resulting value is relative to larger and larger argument values, I just used a few if statements to decided. Again, not pretty but it gets the job done.

The result of the rotation loop is a copy of the original array that has every element in its rotated position. The rotation is not done in place, but I’d say constant space and linear time complexity is fine.

If you understand the rotation code, the rest of the indexing in the program is straightforward since it’s all just variations of this concept.

One part of the program that I’m particularly happy with is how clearing lines is accomplished. Since the playing field is a long array of characters, a completed horizontal line must be deleted and every index above must be shifted down that many units. To accomplish this, I was able to make use of the standard librarys std::rotate() function.

// i is the current value in a for loop of a linesToBeCleared array
std::rotate(game.inactivePieceBuffer.begin(),
game.inactivePieceBuffer.begin() + (i * game.displayWidth),
game.inactivePieceBuffer.begin() + ((i * game.displayWidth) + game.displayWidth));

Cplusplus.com defines the function as: “Rotates the order of the elements in the range [first,last), in such a way that the element pointed by middle becomes the new first element.” If you make the “middle” argument of the function the first element of the line that needs to be cleared and the “first” and “last” arguments the beginning of your array to shift and the last element of the cleared lines index respectively, the result of this function will be an array with the cleared line(s) rotated up to the top of the playing field array and everything above it shifted down exactly as many units as was cleared!   

TODO

As far as work left, I can definitely add nicer animations and menus, but my original goal for this project has been met. There is still an annoying bug with my console printing that leaves a digit of the previous game’s cleared lines value on the screen during the game immediately following a game over that I haven’t figured out how to fix yet. Additionally there are also a few common features missing such as the next piece hint.

Otherwise, that’s it! Feel free to check out the full source code on my Github profile.

Leave a Reply