How to re-implement Tetris

I'm one of those programmers who likes to implement everything on my own. No, I don't mean that I don't trust the work done by others. Rather, I believe that if I do something myself, it will be much more useful than if I just take someone else's implementation. For example, I wrote my own regular expression parser in C, and used my own C data structure library. I hope to write more about this someday.

Without a doubt, I have gained a ton of experience and knowledge from doing all of these things myself. Therefore, in my opinion, it is quite advisable to continue to do this, that is, to try to implement some project that has already been implemented previously. All this is for the purpose of self-education. This time I took it upon myself to redesign Tetris. To make it even more interesting, I decided to do everything in C.

In fact, this is not my first game in C – a little earlier I had already written a clone in C sapper. I also didn’t write about this on the blog, but maybe I will write in the future, since the project turned out to be very interesting. Perhaps this experience was useful to me when working on Tetris.

Creating a GUI in C

In game development, it is especially important that you have to pay special attention to interface development. I have a lot of experience writing libraries (that is, code that other developers can use in their programs). Libraries are good. You, as a programmer, think about how, if you were another person, you would use the service that your library provides. You come up with names for all the functions and also describe how these functions should work. Write examples with code that uses your hypothetical library so that you can test how convenient this code is. Then you start implementing your functions. There is no “user interface” as such. You spend all your time “thinking like a programmer,” which is intellectually very interesting. But software, as a rule, is written for users, and games are the most user-oriented class of software.

There really isn't much in the way of creating user interfaces in C. This language uses a classic command line interface (read from the console, output to the console). An interface of this kind is very convenient, since you have to work exclusively with text for both input and output. But, unfortunately, it is not only unsuitable for most games, but also scares users – as a rule, they recoil screaming from the screen with the command line (that is, from the line where you need to enter commands).

The other extreme of GUI programming in C is the world of windowing tools. Using such libraries, you can create windowed applications similar to those you are used to working with on a PC. There are not one or two such tools. Of course, Microsoft Windows has its own similar tools. Linux has an extensive collection of such tools, with GTK coming to mind first. Unfortunately, any toolkit for creating windowed applications in C requires writing a lot of boring and difficult-to-understand code. This is quite logical: the C language is quite low-level. In general, GUIs are generally difficult to describe in programming languages. The closer a language is to machine code, the more work you have to do to express in code what your interface should look like.

Fortunately, there is an excellent middle ground in this case. It is in C that you can quickly write a Tetris-class game. Let's not type code into the command line and read the output, but try to draw a user interface directly in it? Since the console screen is just a grid of symbols, it will be easy to program a Tetris-like game on it, focused on moving around the squares. It turns out that interfaces in this style are quite common (especially in programs written for Linux/Unix). In addition, there is one universal library for building such interfaces, it is called ncurses.

Armed with a library ncurses, you can implement some very cool things in the command line window. Despite the fact that in a typical C program you can only add text to the command line field (type it), in programs using ncurses You can move the cursor in the command line window, as well as place individual characters anywhere on the screen. Thus, using the library ncurses you can assemble a user interface with many interactive functions right in the command line window.

Making Tetris

So, I had no doubt right away that I wanted to write Tetris using

ncurses

. It was necessary to implement game logic. I aimed to keep the gameplay logic completely separate from the UI logic. To do this, I divided the code into two files:

 tetris.c

And

main.c

. File

tetris.c

has no idea that there is a user interface, since all the user interface processing is handled by main.c. Likewise,

main.c

knows nothing about how exactly Tetris is implemented, that is, what information is provided in the header file

tetris.h

. All this is easily explained. You should write code that does exactly one thing, and does it well. If you approach this carelessly and do two things in the same code (for example, implement the rules of the Tetris game in the user interface), then most likely you will simply confuse both functionality. Plus, an important bonus with the approach I'm taking is that I can write a completely new interface for my Tetris game without even touching the file

tetris.c

.

Game logic

At first I thought that writing Tetris would not be difficult. But after researching the problem a little, I became convinced that it is much more complicated than it seems. For example, we take it for granted that when we turn a block near a wall, it bounces off it (rather than getting stuck near it). These specific behaviors (and many others) need to be kept in mind as you develop this game.

I started with a simple game loop and gradually expanded on it. Function tg_tick() (Here tg – This tetris_game) manages to complete one iteration in one game cycle. It looks like this:

/*
  Выполняем один шаг игры: обрабатываем тяготение, пользовательский ввод и обновляем счёт. Возвращаем true, если игра продолжается, и
  False, если игра проиграна.
 */
bool tg_tick(tetris_game *obj, tetris_move move)
{
  int lines_cleared;
  // Обрабатываем тяготение.
  tg_do_gravity_tick(obj);

  // Обрабатываем ввод.
  tg_handle_move(obj, move);

  // Проверяем, удалились ли какие-нибудь линии
  lines_cleared = tg_check_lines(obj);

  tg_adjust_score(obj, lines_cleared);

  // Возвращаем данные о том, продолжится ли игра после данного хода (NOT, если она проиграна)
  return !tg_game_over(obj);
}

Let's look at this code line by line. Let's start with

tg_do_gravity_tick()

. In the game Tetris, a falling block moves downward under the influence of gravity. The more difficult the level, the faster the block moves. Thus, the gravity tact function will count a delay, after which it will act again (pull the block down). When it is time to move the block down, the function will do so and then reset the timer. In this case, the time until the next act of gravity will be calculated depending on what difficulty level you are playing at.

After the gravity tick, the game processes user input by calling the function

tg_handle_move()

. This function takes the value

tetris_move

, which can correspond to any of the moves possible in Tetris: move right, move left, throw a block, hold a block. The function performs this move and then returns.

Now that both gravity and user input have been processed, the possibility arises that some lines were filled from edge to edge. Therefore, we call the function tg_check_lines(obj)to count such lines and, if any are found, delete them. We then update the score based on how many lines were cleared. The scoring of points depends on the level of the game and on how many lines you managed to remove on a given move.

Finally, in the UI code that calls this function tg_tick()you need to be able to know when the game is over. So, tg_tick() returns truewhile the game continues, and falsewhen it's over.

To describe the game logic of Tetris, a lot more code is required – the total size of the file tetris.c is almost 500 lines. It’s impossible to cover it all in one post. It's quite interesting because this code needs to recognize not only all types of tetrominoes, but also the orientation of each of them. This is also where the code needs to detect collisions and handle hitting a wall when you rotate a shape. If you are interested in how exactly this is all done, you can study it in more detail, it posted in the GitHub repository.

User interface

Of course, all the above code describes the game logic, but does nothing to display the game to the user. It simply changes the structure of the game in memory. Gameplay display and user input processing are contained in the file

main.c

.

I would like to go into detail about the main function from main.c, but it seems too big for this post. It's not that difficult to understand, it's just a lot of lines, and most of its specific details are not important in our case. But I can explain quite clearly how it works using just pseudocode.

int main(int argc, char **argv)
{
  // Если пользователь указал имя файла, загрузить игру, сохранённую под таким именем 
  // В противном случае начать новую игру.

  // Инициализировать библиотеку ncurses.

  // Выполнить главный игровой цикл:
  while (running) {

    // Вызвать функцию tg_tick(), чтобы игра двигалась вперёд.

    // Отобразить новое состояние игры.

    // Ненадолго заснуть (иначе игра пойдёт слишком быстро).

    // Получить пользовательский ввод для следующего цикла.
  }
}

If you want to take a closer look at the UI code,

look at the file

main.c

in the GitHub repository.

Finished product

Finally, my simple implementation of Tetris is almost ready. After working for just one day, I programmed most of the Tetris features myself:

  • Basics (ie, gravity, movement, turning and erasing lines).
  • Saving blocks with the possibility of their subsequent replacement.
  • A scoring system copied from earlier versions of Tetris.
  • Progressively more difficult levels the longer you play.
  • The pause menu (including the “boss version” of such a menu, when you go to it, an imitation of the command line appears in place of the game, so that it seems as if you are working.
  • Ability to load and save games so you can return to previously started games.

The only thing I couldn't do was play the Tetris themed soundtrack in the background of the game. Maybe I'll get back to this someday, but C doesn't provide any convenient ways to simply play a melody.

If you want to try it yourself, it's best to do it on Linux. You will need to install the library ncurses (on Ubuntu you will need to run sudo apt-get install libncurses5-dev). Then go to GitHub repositorycompile the code using make and execute using bin/release/main.

Conclusion

This post details how to implement a Tetris clone. And frankly, I think this topic deserves discussion. I think I designed the program with dignity, I am pleased to look at code like this

tg_tick()

. Moreover, this program is truly playable, and this in itself is good. True, I would like to end the article with a big philosophical digression about the importance of repeated implementations.

When I told my girlfriend that I was writing my own version of Tetris, she immediately responded along the lines of “isn’t that already done”? A completely normal reaction, because, in fact, it has already been done. If this criterion were the defining criterion when programming, then most of the code that I wrote in my life simply would not exist. Of course, we can talk a lot about how important it is to do something new and unique, and even more can be said about reusing code. But doing something old and banal is not nearly as boring as it seems at first glance. Practice is the path to mastery, and exercises in cloning known, important, and even great programs (e.g. shells, regular expressions, web servers, firewalls And other games) is the best way to improve your programming skills while expanding your domain-specific knowledge by leaps and bounds.

It is normal for a programmer to know what loops, conditionals, functions and classes are. These are baked like hot cakes at universities. Knowing only these concepts, you can already achieve a lot, but, in my opinion, this is only the beginning of the path to much more exciting self-education. When you learn to solve real applied problems, only then will you have a chance to take on things that you may have been told about at university, but you never learned because you didn’t implement them yourself. In addition, when solving such problems, you have to understand industry specifics, for example, Linux, HTTP, TCP, ncurses, GTK, …), which no one has ever taught you, but sooner or later you will have to use such things every day at work. Moreover, even if you do not turn to a specific subset of knowledge, you will still benefit, since you will broaden your horizons and master new tools and approaches that you would otherwise never have used before.

In short, these “re-implementation” projects became a vital part of my education, complementing the computer science theory I learned at university. Without these projects, I definitely would not have become the programmer I am now. Now I literally think in C. Pointers, arrays, structures, bits, bytes, system calls are now inseparable from my personality. I understand how programs implement those things that we take for granted: for example, how processes are created, threads are generated and exchange information. I can tell you for hours how packets are routed across the Internet, how the firewall sifts through them (especially on Linux). I love Chomsky's hierarchy and love to talk to those who will listen about the pure theory of regular expressions and finite state machines, how it led to the implementation of regular expressions – one of the most common and sought-after tools of a programmer today.

Thanks to the experience and understanding that I gained on re-implementation projects, I learned to think more effectively and see connections between old and new tasks. I'm always thinking about how to take advantage of the best ideas. I better understand why certain decisions were made during the design. I can approach problems in the same disciplined manner that other implementations I've studied have been done. I was able to combine the scattered knowledge that I acquired into a completely new set of extensive knowledge and better approaches, and also realized how little I knew.


You might also want to read this:

Develop and develop your game (and more) with the help cloud hosting for GameDev

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *