So, last Monday I started a little game project as an excuse to experiment with my ConsolePaint library for .Net, but also as an exercise for a much more ambitious project later (which will be fully retro). For this occasion, I thought I'd replicate one of my favourite games of the NES; Tetris, but with a twist; the user should be able to create his own difficulty by directly modifying drop delays, game execution speed and the rate at which the game becomes faster (and thus, more challenging).
There are a couple of challenges in doing a game in the console terminal:
Printing characters to the console window is slow.
Refreshing the screen is even slower and just printing the characters on new lines wouldn't entirely sell the illusion of a piece moving.
You can only use characters when using the terminal, so no graphics (duh!)
In this case, the "user interface" needs to be user friendly enough so that anyone can play and alter the game variables without having to remember the command names.
There needs to be some kind of sound for tetrises and background music.
I couldn't really solve the first problem since .Net doesn't have any way to hijack the console screen buffer like in C++ with windows.h, so I ended up trying to minimize as much as possible calls to Write and WriteLine by only writing and rewriting in precise areas of the screen by repositioning the cursor. Because some areas of the screen are only drawn once or occasionally, it saved some execution time. This did more to solve the second problem than the first one actually, since it entirely got rid of the slow refresh problem you'd get with the Clear method.
First shot shows the areas where the characters are only printed once on the screen. Second shot shows areas which are only reprinted when the information needs to be updated.
So the thing with Tetris is that the game is basically just squares on a screen. You don't need to be particularly fancy with the graphic design and people know this (except maybe the folks who made Tetris Effect haha). However, I still wanted to use the most out of the character set to make it as pleasant to the eye as possible. This is where the lack of graphics really hurts because the .Net console only supports 16 colours and a limited set of characters. I did notice also that the pieces in classic nes Tetris actually don't always use the same palette and appearance, so I at least wanted to include alternating colours and use fun characters as alternative tetromino blocks. However, after I started showing pictures on a few discord groups and to friends, all were unanimous that the special characters HAD to go and at least half of them found the colours jarring and too much to process. That made me sad because I really liked the fun character set and I sincerely thought the colours made the experience better. But I aim to please everyone, including I. So what I ended up doing is making those things optional; they can be turned on and off in the game settings.
The above screenshot shows a game where the special characters are disabled with colour enabled, this one below is the reverse. These options are not mutually exclusive by the way; you can have grey bland tetrominos or colourful tetrominos using the fun characters.
"The fool did not see that he was not Tetris ready. He was severely disappointed!"
Now, there remains the case of the GUI. If I was going to write code for something which wouldn't make people flee, it would take a while. Remember earlier when I wrote that I was looking for an excuse to use my ConsolePaint library? Well, I put it to good use. All these rectangles on the screen were "drawn" by it, I just called the right methods and it wasn't even something I had to worry about. The library can draw all sorts of different borders too and initially I tried a more varied design until I ended up with what we have now. I also used that library to draw a picture on the screen so that the main menu didn't look too empty. What I did first was to draw some quick pixel "art". The colours of the image were converted to the 16 colour palette of the .Net console and then the pixels were converted to characters. Although in this case, the conversion was probably not needed. The image I drew:
You may be surprised to see that I named my Tetris clone "Retro Tetro Pro". That's actually because the name Tetris is trademarked. Maybe I'm being overly cautious, but I want no trouble for a game which only a few number of people are going to play.
I'm not going to talk to much about the user friendliness of the GUI, I think navigating the menus with the arrow keys and pressing enter is a pretty obvious design choice. However it may or may not surprise you to learn that I didn't use a switch statement or if statement for handling user choices in the menu. That would have been too long and tedious! Instead I chose to put some actions in an array and the arrow keys actually increase or decrease an index which is used to call those actions. I wish I had thought of that before. Strictly speaking of design though, the only thing I consciously thought about for more than one second is how to give an obvious visual cue that a value is being increased or decreased. I chose to print a green upward triangle when the user is increasing a value and red downward arrow when the user is decreasing the value. You can see more of the GUI and the effects the options have on the game in this little demonstration video: https://www.youtube.com/watch?v=MuE_L0-kAlI
Those who went ahead and watched the video will notice that the game only plays a sound when its in the menu... That's actually a reflection of the how bad it is to use sounds in .Net. There was nothing I could do to really play music or fun sounds without it being a huge pain in the neck. I tried several solutions and ways to circumvent the problem, but the tools .Net comes with are insufficient for a pleasant experience. I figure if I have enough time and motivation in the future that I'll have to use a custom sound library with .Net bindings; Beeps are too annoying and trial-and-error based to be practical and the SoundPlayer is incredibly frustrating; it has its own little thread and it does its own little thing, so no way to directly interact with it or do "complex" algorithms like having a playlist of music or playing consecutive sounds. It plays the tune until its over or until the process is dead and sometimes its stop method inexplicably doesn't work. Also there's no way to tell when the SoundPlayer is done... not mentioning several limitations like being limited to playing wav files out of anything. At this point, I was a bit tired of working on this project and anyways, to me, one of the rare people on earth who does not enjoy music due to a medical condition, it was the least of my priorities.
Thus, I felt it was a job well done and decided to release the game to a slightly wider audience with the bugged music player (it can be enabled in the menus, but it is disabled by default). I haven't gotten a lot of feedback yet because well... it's a Tetris clone. Everyone and their grandmother has both played and made it before. So I can't tell you if it was a success from the point of view of others. But I can tell you I'm satisfied with what I came up with despite the fact that I wasn't able to solve every issue I had. I learned a lot of things by putting myself to the challenge of making it work in the terminal window and it was a pretty interesting experience which I don't regret.
Thank you for reading this post. And if you try Tetro Retro Pro, thank you once more, it makes me incredibly happy that it will not all be merely for showing off and experimenting.
You can download the game here.
Edit: For a much lower download size, download this version instead. In this version, any reference to music was commented out. Saved settings are not compatible between the two versions.
Edit 2: Someone on discord pointed out that it doesn't come with info about the controls; Arrow keys to move and z to rotate.
Edit 3: New release. Added controls information to the menu, added a jingle for a quadruple line clear and added the pause functionality.