Checking out Tetris for BASH

Jarret B

Well-Known Member
Staff member
Joined
May 22, 2017
Messages
336
Reaction score
360
Credits
11,504
I know some of you may not be into programming. I cannot explain enough how important it can be to be able to create scripts. Automate everyday simple tasks into a script is an important ability. For anyone who remembers the DOS day, batch files could be important. BASH is much more powerful and is in its own right, a language. It just isn't compiled into an executable file. Trust me, there are some very important items in this article that can enhance a BASH script. This article is not just about Tetris, but all the commands that BASH uses to make it work. These can be implemented in any script.

By no means am I taking any credit for this BASH Tetris program. I found the BASH code on the Internet at 'https://github.com/dkorolev/bash-tetris'.

For anyone who remembers when Tetris came out in the 80's, this article is for you. Also, if you have not seen the 'Tetris' movie, I strongly suggest you watch it. Unfortunately, it is on AppleTV. It is intriguing how many companies made money off of the game, and yet did not have the rights to the game.

The Code

Before you jump on over to GitHub, just grab the code from the attachment in the section below the article. You can see the game in Figure 1.

Figure 1.JPG

FIGURE 1

After copying the script from the Attachments (it is listed as .txt file), rename it to '.sh' or just make it excutable.

What I found with the BASH code, is two things. One, the main section of the code is spread out between the functions. Yes, you can do this, I will demonstrate it in a bit. The second thing is that there is a coding error, which is fixed in the code below, but not yet on the GitHub site. Around line 310, is the line '((next_piece_color = RANDOM % ${#colors[@]}))'. The line should be '((next_piece_color = RANDOM % ${#colors[@]+1}))'. Originally, there is a random number from 1 to 7 being generated. The number needs to be 1 to 8 since the value '0' represents black. A black block on the screen is not readily visible unless your background is white and not black.

Let's cover the details about code being spread out between the functions. Any code not part of a function is considered part of the main section. Let's look at an example:

#!/bin/bash

a=1
function add_1 {
((a+=1))
}

a=2
b=1

function add_2 {

((a+=2))
}

a=3
c=3
echo $a, $b, $c
add_1
echo $a, $b, $c


The output is as follows:

3, 1, 3
4, 1, 3
6, 1, 3


The variable 'a' is the only one being changed. Even though 'b' is set between functions, it still works.

Overall, there are 37 functions in the Tetris program as well as the main section after the functions (once I moved everything to the same area for readability).

Main Section

The main section of code, after the functions, is mainly used to set up variables. One of the things you may have noted before the functions is a line 'set -u'. The command requires all variables to be initialized to a base value. You cannot just start using a new variable without initializing it, or it will result in an error.

For instance, if I have a script with the line 'echo $a'. That is the only line in the shell script, it will print a blank line. If I make the first line 'set -u' and run the script, I get an error. The error is that there is an unbound variable or one that has not been defined. You do not need to define a variable by giving it a base value, like 'a=1'. It could be initialized by a calculation, such as 'a=$(((b*5)-2))'.

Now, let's look at the main section. The first uncommented line is very important. The line is 'trap '' SIGUSR1 SIGUSR2'. The first parameter is two single quotes. We are capturing two user-defined interrupts.

Interrupts

Let's look at this a bit. Interrupts can get quite interesting.

There are 64 interrupts on a Ubuntu system. To see the 64 interrupts, use the command 'trap -l'. The result is:

1) SIGHUP 2) SIGINT 3) SIGQUIT 4) SIGILL 5) SIGTRAP
6) SIGABRT 7) SIGBUS 8) SIGFPE 9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG 24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO 30) SIGPWR
31) SIGSYS 34) SIGRTMIN 35) SIGRTMIN+1 36) SIGRTMIN+2 37) SIGRTMIN+3
38) SIGRTMIN+4 39) SIGRTMIN+5 40) SIGRTMIN+6 41) SIGRTMIN+7 42) SIGRTMIN+8
43) SIGRTMIN+9 44) SIGRTMIN+10 45) SIGRTMIN+11 46) SIGRTMIN+12 47) SIGRTMIN+13
48) SIGRTMIN+14 49) SIGRTMIN+15 50) SIGRTMAX-14 51) SIGRTMAX-13 52) SIGRTMAX-12
53) SIGRTMAX-11 54) SIGRTMAX-10 55) SIGRTMAX-9 56) SIGRTMAX-8 57) SIGRTMAX-7
58) SIGRTMAX-6 59) SIGRTMAX-5 60) SIGRTMAX-4 61) SIGRTMAX-3
62) SIGRTMAX-2 63) SIGRTMAX-1 64) SIGRTMAX


So, we are looking at using SIGUSR1 (10) and SIGUSR2 (12). Each interrupt has its own value. You can use the name, SIGUSR1, or the number, 10 when referencing the interrupt. SUGUSR1 and SIGUSR2 are user-defined interrupts that we use to control the gameplay of Tetris. It is the timer that causes the pieces to fall (SIGUSR1). When we quit the game, we use SIGUSR2.

The way 'trap' works, is we can associate a command with each specific interrupt. Let's look at an example:

#!/bin/bash
trap "echo The script is terminated; exit" SIGTERM
trap "echo The script is interrupted; exit" SIGINT
while true
do
echo Script is Running in PID $$ ....
sleep 1
done


The first trap command sets up commands to be executed when the script is 'terminated' (SIGTERM). The SIGTERM (15) interrupt is caused when a process is terminated. We can terminate a process by issuing the 'kill' command with the Process ID (PID) of the process. So, when the process is terminated, the trap will issue an 'echo' command saying the script is terminated and then will exit.

The second trap command is set to echo that the script is interrupted and exit if the SIGINT interrupt is received. Since it is a script, we can simply press CTRL+X to interrupt the script.

Other than that, the script is a loop that continues until it is either Terminated or Interrupted.

The special variable '$$' holds the Process ID of the current program or script. Since we know the PID, we can use the 'kill' command from another terminal to Terminate the script.

In the Tetris games, we are not specifying a command to run when SIGUSR1 and SIGUSR2 are trapped, we are just holding them for now.

In the game, SIGUSR1 is managing the timer, while SIGUR2 is waiting for the user to press 'q' to quit.

Variables

After we trap the interrupts, we set up a list of variables with a default value.

The initial variables are for the controlling of the play. For example, 'QUIT' is '0'.

The next set of variables sets the delay between the piece falling each time a specific delay is set. Then, there is a 'DELAY_FACTOR' which controls the change in the delay when the player levels up. Of course, the higher the level, the faster the blocks will drop.

Then we set up the color values one through seven from 'RED' to 'WHITE'.

Next, we set up the playing field size information. I must say, there is no coding to set the minimum size of the screen. The screen needs to be a minimum of 23 lines and 77 columns. So, I will add my 'screensize', from the BASH Dice Game (https://www.linux.org/threads/bash-dice-game.45645/) function to allow the screen to be a minimum of 80x25 characters. The function is:

function screensize {
col=tput cols
row=tput lines
if [[ col -lt "80" ]]
then
col=80
fi
if [[ row -lt "25" ]]
then

row=25
fi
printf '\e[8;'${row}';'${col}'t'
}


Instead of using the 'printf' command, I could use the 'resize' command, but this may not be available on all systems without being installed.

Next, a few sections are setting up the location of the various areas of the playing field. The location of the areas for the score, help information, next piece information, and gameover data.

We then set the variable for how often the delay changes for the pieces to fall. In the case of this script, the delay is decreased every 20 levels.

We then set default variables for settings, the screen, etc. For example, we set the value for the 'empty_cell' as a period. If we set it as a space, there would be no markers on the playing field. We also set the 'filled_cell' as '[]'. For the 'empty_cell,' you could make it equal to '..' and not ' .'. Also, the 'filled_cell' could be two UTF-8 code '2588' characters. I placed these in as part of the remark. If you want to try it, just move the new characters over. The brackets ([]) aren't changed unless you remove the color by pressing 'c' in the game, but the 'empty_cell' works on both color and non-color mode. With color, we just set the foreground and background color to the same in color mode, so the whole block is a solid color. Just keep in mind that each cell, empty or filled, is two characters wide.

The next section is to set the variable 'help' to a long string of text.

We finally get to the part of setting up the seven blocks that we have to place on the playing field as they fall. Each piece is made up of only four blocks in various ways. The code on the seven lines, put in the array 'piece', specifies the X and Y coordinates of each little block that make up the piece. Each line also specifies how it will look when rotated. Each piece has 1, 2 or 4 variations, depending on rotation. The square block is the same no matter how it is rotated, so it has one orientation. The long line piece, 4 blocks long, only has two possible orientations. The code is as follows:

piece=(
"00011011" # square piece
"0212223210111213" # line piece
"0001111201101120" # S piece
"0102101100101121" # Z piece
"01021121101112220111202100101112" # L piece
"01112122101112200001112102101112" # inverted L piece
"01111221101112210110112101101112" # T piece
)


The pieces are all shown in Figure 2. The image only shows the first orientation, or first 4 coordinates (8 numbers), of each piece.

Figure 2.JPG

FIGURE 2

The code goes in to set more defaults, and saves the terminal information to 'stty_g'.

Then comes the very important part of the whole script. It runs 'ticker' as a separate process, adds 'reader' to it, and pipes it to 'controller'. This is done until the user presses 'q' or the process is terminated in another manner. After this, the cursor is shown ('show_cursor'), and the terminal information is restored from 'stty_g'.

I'll have another article to cover the functions, or at least the important ones soon.

Conclusion

The coding outlined in this example is the basis for all 'text-based' games, or at least those with basic graphics.

If you look back at the 80's and even early 90's, these games are all based on the same layout. Games were only improved with better hardware, both processor and graphics.

If you can learn these principles, then you can recreate a lot of games.
 

Attachments

  • tetris.txt
    16.7 KB · Views: 155
Last edited:


That's pretty impressive, i would think gaming with a command line terminal would be pretty niche, but it can certainly be done. I found somebody who made the first level of NES mario using sed and bash. They used escape characters to make the level, i see this game has used tputs for some of the graphical features.

Video games are some of the most sophisticated uses of programming, it's important to start small if you get into that. However, python gives novices a pretty easy way to start game devving right away.
 

Members online


Top