Introduction
In this project, you will learn how to create a simple typing game using a shell script. The game displays random characters on the screen, and your goal is to type them before they disappear. The game offers different modes with varying difficulty levels. You can choose to practice typing numbers, letters, a mixture of both, or even your own custom words.
👀 Preview

🎯 Tasks
In this project, you will learn:
- How to create a project file and open it in a code editor
- How to display a welcome interface using special characters and colors
- How to implement a mode selection menu for choosing the difficulty level
- How to implement a typing category selection menu for choosing the type of characters to practice
- How to create functions for drawing a border and filling the background color of the typing interface
- How to generate random letters and numbers for the typing game
- How to implement the typing functionality, including handling user input and calculating accuracy
- How to create a graceful exit function to handle special signals
🏆 Achievements
After completing this project, you will be able to:
- Demonstrate shell scripting fundamentals
- Use special characters and colors in terminal output
- Read input from the user in shell scripts
- Implement menus and user interfaces in shell scripts
- Handle special signals in shell scripts
Create Project File
To Start, Please Create a New File Named shell_typing_game.sh and Open It in Your Preferred Code Editor.
cd ~/project
touch shell_typing_game.sh
Display Welcome Interface
#!/bin/bash
function dis_welcome() {
declare -r str='
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000
1000000010000000101111101000000111100111000000100000001000001111
0100000101000001001000001000001000001000100001010000010100001000
0010001000100010001111101000001000001000100010001000100010001111
0001010000010100001000001000001000001000100100000101000001001000
0000100000001000001111101111100111100111001000000010000000101111
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000
0000000000001111000000100000000001000000010000001111100000000000
0000000000010000000001010000000010100000101000001000000000000000
0000000000010011000011111000000100010001000100001111100000000000
0000000000010001000100000100001000001010000010001000000000000000
0000000000001111001000000010010000000100000001001111100000000000
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000'
declare -i j=0
declare -i row=3 ## Print start line.
line_char_count=65 ## Define the newline position, 64 characters per line plus newlines, for a total of 65 characters.
echo -ne "\033[37;40m\033[5;3H" ## Set the color and cursor start position.
for ((i = 0; i < ${#str}; i++)); do
## Line feed.
if [ "$((i % line_char_count))" == "0" ] && [ "$i" != "0" ]; then
row=$row+1
echo -ne "\033["$row";3H"
fi
## Determine foreground and background characters.
if [ "${str:$i:1}" == "0" ]; then
echo -ne "\033[37;40m "
elif [ "${str:$i:1}" == "1" ]; then
echo -ne "\033[31;40m$"
fi
done
}
dis_welcome
The long string at the beginning 010101... is the welcome game interface to be displayed. If you look at it directly, you may not understand what it is. So you can copy this string to a gedit editor and then press the shortcut Ctrl+F to find. When you enter 1, you will understand it immediately. Let's focus on the echo command. We need to use the echo command because we want to display it on the screen (here it refers to the Xfce terminal. The standard input, output, and standard error output under Linux are all connected to the terminal by default). To achieve some special functions, such as printing at a specified position and setting the display character color, and print the background of the entire game, we need to use some special parameters of the corresponding command, as shown in the following code:
## Set the color and cursor start position.
echo -ne "\033[37;40m\033[5;3H"
Based on the comments, we can roughly understand its function. But what exactly does it mean? First, the -n parameter means to print in the current line without line feed after output. Then set the output character color and cursor position. Among them, \033[foreground color;background color m, and the following is \033[row;column H. However, to make echo recognize these special escape sequences, we need the -e parameter, otherwise echo will output these characters as they are.
Foreground colors: 30-39, background colors: 40-49.
| Value | Foreground Colors | Value | Background Color Range: 40 - 49 |
|---|---|---|---|
| 30 | Set black foreground | 40 | Set black background |
| 31 | Set red foreground | 41 | Set red background |
| 32 | Set green foreground | 42 | Set green background |
| 33 | Set brown foreground | 43 | Set brown background |
| 34 | Set blue foreground | 44 | Set blue background |
| 35 | Set purple foreground | 45 | Set purple background |
| 36 | Set cyan foreground | 46 | Set cyan background |
| 37 | Set white foreground | 47 | Set white background |
| 38 | Set underline on default foreground color | 49 | Set default black background |
| 39 | Set underline off default foreground color |
Didn't expect echo to be used like this! Do you feel like you've reacquainted yourself with it? Okay, now let's test this piece of code in the terminal.
cd ~/project
bash shell_typing_game.sh

Display Mode Selection Menu
## Declare the variable 'time' to represent the typing timeout, where different difficulty levels correspond to different timeout values
declare -i time
function modechoose() {
## Select from three different modes
echo -e "\033[8;30H1) easy mode"
echo -e "\033[9;30H2) normal mode"
echo -e "\033[10;30H3) difficult mode"
echo -ne "\033[22;2HPlease input your choice: "
read mode
case $mode in
"1")
time=10
dismenu ## Call the menu selection function
;;
"2")
time=5
dismenu
;;
"3")
time=3
dismenu
;;
*)
echo -e "\033[22;2HYour choice is incorrect, please try again"
sleep 1
;;
esac
}
Before defining this function, we first declared an integer variable 'time' using the command declare -i. Then, in the following case statement, we set different values for the 'time' variable based on the user's difficulty selection. The higher the difficulty, the smaller the value. This variable is actually used in the subsequent typing processing function. You may remember that in shell scripts, variables declared or defined inside or outside a function are treated as global variables with a scope throughout the script file, unless the 'local' keyword is used to declare the variable within a function. As for the display effect of the menu, we still use the echo command and then use the read command to read the user's input into the variable 'mode'.

Display Typing Category Selection Menu
function display_menu() {
while [ 1 ]; do
draw_border
echo -e "\033[8;30H1) Practice typing numbers"
echo -e "\033[9;30H2) Practice typing letters"
echo -e "\033[10;30H3) Practice typing alphanumeric characters"
echo -e "\033[11;30H4) Practice typing words"
echo -e "\033[12;30H5) Quit"
echo -ne "\033[22;2HPlease input your choice: "
read choice
case $choice in
"1")
draw_border
## The next two are function parameters, the first parameter indicates the typing type, and the second is the function for moving characters
main digit
echo -e "\033[39;49m"
;;
"2")
draw_border
main char
echo -e "\033[39;49m"
;;
"3")
draw_border
main mix
echo -e "\033[39;49m"
;;
"4")
draw_border
echo -ne "\033[22;2H"
read -p "Which file would you like to use for typing practice: " file
if [ ! -f "$file" ]; then
display_menu
else
exec 4< $file ## Create a file pipeline
main word
echo -e "\033[39;49m"
fi
;;
"5" | "q" | "Q")
draw_border
echo -e "\033[10;25HYou will exit this game now"
echo -e "\033[39;49m"
sleep 1
clear
exit 1
;;
*)
draw_border
echo -e "\033[22;2HYour choice is wrong, please try again"
sleep 1
;;
esac
done
}
In this function, let's first explain the two functions called in the case branch. First, draw_border function is used to draw the border of the typing interface, which will be shown later. Then, the main function is called. This main function does not have any special role like the main function in languages such as Java and C, it just simply has the name main, which is used to indicate that it plays a major role in the entire program. You may have noticed that there is an argument after each main, yes, this is the parameter passed to the function. In shell, parameters are not written inside parentheses immediately following the function name, unlike many other languages. It should also be noted that in the fourth branch of the case statement, which is these lines:
read -p "Which file would you like to use for typing practice: " file
if [ ! -f "$file" ]; then
display_menu
else
exec 4< $file ## Create a file pipeline
main word
echo -e "\033[39;49m"
fi
According to the menu prompt, we can know that this branch is for practicing typing words. This program allows users to use custom word files (text files with a certain formatting requirement, with one word per line, you can download a word list file online and use the awk command to extract) for practice. So first we need to read in a user input file name to select the word file. Then we use the exec command to create a pipeline that points to the file, i.e., redirecting the output of the file to file descriptor 4 (fd). Since exec is a built-in command in bash, you won't be able to see the documentation of exec using the man command. I/O redirection with exec is usually related to fd, and shell usually has 10 fd, ranging from 0 to 9. The common used fd are 3, which are 0 (stdin, standard input), 1 (stdout, standard output), and 2 (stderr, standard error output). Just understand its meaning for now.

Drawing a Border for a Typing Interface
function draw_border() {
declare -i width
declare -i high
width=79 ## terminal default width - 1
high=23 ## terminal default height - 1
clear
## Set display color to white on black background
echo -e "\033[37;40m"
## Set background color
for ((i = 1; i <= $width; i = i + 1)); do
for ((j = 1; j <= $high; j = j + 1)); do
## Set display position
echo -e "\033["$j";"$i"H "
done
done
## Draw background border
echo -e "\033[1;1H+\033["$high";1H+\033[1;"$width"H+\033["$high";"$width"H+"
for ((i = 2; i <= $width - 1; i = i + 1)); do
echo -e "\033[1;"$i"H-"
echo -e "\033["$high";"$i"H-"
done
for ((i = 2; i <= $high - 1; i = i + 1)); do
echo -e "\033["$i";1H|"
echo -e "\033["$i";"$width"H|\n"
done
}
- In the
draw_border()function, two integer variableswidthandhighare declared, which represent the width and height of the terminal window respectively. - The command
echo -e "\033[37;40m"is used to set the display color. Nestedforloops are used to iterate through each row and column of the terminal to fill the background. - Within the inner loop, ANSI escape codes are used to position the cursor at the desired coordinates within the terminal. For example,
echo -e "\033["$j";"$i"H"sets the cursor position to row j and column i. - After filling the terminal background, decorative borders are drawn using specific characters. The characters used include
+,-, and|, which are commonly used for drawing borders. The positions of these characters are determined by their row and column values, and printed using ANSI escape codes. For example,echo -e "\033[1;1H+"places the+character at the top left corner of the terminal.
In summary, the draw_border() function clears the terminal, sets the background color to black, fills the terminal with spaces to create a background. Finally, it draws a visually appealing border by placing characters (such as + for corners, - and | for lines) at specific positions.

Filling the Background Color of the Typing Interface
## Clear the entire character landing area
function clear_all_area() {
local i j
## Fill the typing area
for ((i = 5; i <= 21; i++)); do
for ((j = 3; j <= 77; j = j + 1)); do
## Set the display position
echo -e "\033[44m\033["$i";"$j"H "
done
done
echo -e "\033[37;40m"
}
## Function: Clear a specific column of characters
## Input: The column number to be cleared
## Return: None
function clear_line() {
local i
## Fill the typing area
for ((i = 5; i <= 21; i++)); do
for ((j = $1; j <= $1 + 9; j = j + 1)); do
echo -e "\033[44m\033["$i";"$j"H "
done
done
echo -e "\033[37;40m"
}
- The
clear_all_area()function:
- It is used to clear the entire character landing area. Its function is to fill the characters in the character landing area with the background color (black) to clear the characters in the terminal window.
- It uses nested loops to iterate over all combinations of
iandj, whereirepresents the row andjrepresents the column. - Inside the inner loop, it sets the cursor position to the specified row and column using ANSI escape codes, and then uses
echo -e "\033[44m\033["$i";"$j"H "to print a space character at that position, but with the background color set to black (code 44). - After the loops, the function uses
echo -e "\033[37;40m"to restore the text color and background color to their default values (white text, black background).
- The
clear_line()function:
- This function is used to clear a specific column in the character landing area. It is typically used to clear the character path in that column after the user enters the correct character.
- The function takes one argument
$1, which represents the column number to be cleared. - It uses a loop to iterate over the rows and columns in the character landing area and fills the character path of that column with the background color (black).
- Similar to
clear_all_area(), the function sets the cursor position to the specified row and column using ANSI escape codes, and then usesecho -e "\033[44m\033["$i";"$j"H "to print a space character at that position, but with the background color set to black. - Finally, the function uses
echo -e "\033[37;40m"to restore the text color and background color to their default values.
Generating Random Letters and Numbers
## Function: Move the character along the falling path.
## Input: Parameter 1: The current row of the character (related to the time length away from the character).
## Parameter 2: The current column of the character.
## Parameter 3: (Unused parameter).
## Return: None
function move() {
local locate_row lastloca
locate_row=$(($1 + 5))
## Display the character to be input.
echo -e "\033[30;44m\033["$locate_row";"$2"H$3\033[37;40m"
if [ "$1" -gt "0" ]; then
lastloca=$(($locate_row - 1))
## Clear the previous position.
echo -e "\033[30;44m\033["$lastloca";"$2"H \033[37;40m"
fi
}
function putarray() {
local chars
case $1 in
digit)
chars='0123456789'
for ((i = 0; i < 10; i++)); do
array[$i]=${chars:$i:1}
done
;;
char)
chars='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
for ((i = 0; i < 52; i++)); do
array[$i]=${chars:$i:1}
done
;;
mix)
chars='0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
for ((i = 0; i < 62; i++)); do
array[$i]=${chars:$i:1}
done
;;
*) ;;
esac
}
## Function: Generate a random character of the corresponding type to be converted into a random character.
## Input: The type of character to be generated.
## Global Var: random_char, array[]
## Return: None
function get_random_char() {
local typenum
declare -i typenum=0
case $1 in
digit)
typenum=$(($RANDOM % 10))
;;
char)
typenum=$(($RANDOM % 52))
;;
mix)
typenum=$(($RANDOM % 62))
;;
*) ;;
esac
random_char=${array[$typenum]}
}
Due to the fact that shell's string variables do not directly support indexing, we need to put all letters and numbers into an indexed array. This is the purpose of the putarray function. The get_random_char function below is used to generate random letters and numbers. It uses the system's random number environment variable RANDOM to obtain a random index, and then reads the corresponding character from the array.
Implementation of Typing
After all the preparations, we can finally start implementing the typing function. Let's take a look at the following code:
function main() {
declare -i gamestarttime=0
declare -i gamedonetime=0
declare -i starttime
declare -i deadtime
declare -i curtime
declare -i donetime
declare -i numright=0
declare -i numtotal=0
declare -i accuracy=0
## Store the corresponding characters into an array, $1 represents the user-selected character type
putarray $1
## Initialize the game start time
gamestarttime=$(date +%s)
while [ 1 ]; do
echo -e "\033[2;2H Please enter the letter on the screen before it disappears!"
echo -e "\033[3;2H Game time: "
curtime=$(date +%s)
gamedonetime=$curtime-$gamestarttime
echo -e "\033[31;40m\033[3;15H$gamedonetime s\033[37;40m"
echo -e "\033[3;60H Total: \033[31;26m$numtotal\033[37;40m"
echo -e "\033[3;30H Accuracy: \033[31;40m$accuracy %\033[37;40m"
echo -ne "\033[22;2H Your input: "
clear_all_area
## Loop 10 times to check if a column of characters times out or is hit
for ((line = 20; line <= 60; line = line + 10)); do
## Check if the column of characters has been hit
if [ "${ifchar[$line]}" == "" ] || [ "${donetime[$line]}" -gt "$time" ]; then
## Clear the display in that column
clear_line $line
## Generate a random character
if [ "$1" == "word" ]; then
read -u 4 word
if [ "$word" == "" ]; then ## End of file reading
exec 4< $file
fi
putchar[$line]=$word
else
get_random_char $1
putchar[$line]=$random_char
fi
numtotal=$numtotal+1
## Set the flag to 1
ifchar[$line]=1
## Reset the timer
starttime[$line]=$(date +%s)
curtime[$line]=${starttime[$line]}
donetime[$line]=$time
## Reset the column position to 0
column[$line]=0
if [ "$1" == "word" ]; then
move 0 $line ${putchar[$line]}
fi
else
## If it has not timed out or been hit, update the timer and current position
curtime[$line]=$(date +%s)
donetime[$line]=${curtime[$line]}-${starttime[$line]}
move ${donetime[$line]} $line ${putchar[$line]}
fi
done
if [ "$1" != "word" ]; then
echo -ne "\033[22;14H" ## Clear the input line characters
## Check user input characters and act as a one-second timer
if read -n 1 -t 0.5 tmp; then
## Successful input, loop checks if the input matches a column
for ((line = 20; line <= 60; line = line + 10)); do
if [ "$tmp" == "${putchar[$line]}" ]; then
## Clear the display in that column
clear_line $line
## If matched, clear the flag
ifchar[$line]=""
echo -e "\007\033[32;40m\033[4;62H right !\033[37;40m"
numright=$numright+1
## Exit the loop
break
else
## Otherwise, always display an error until timeout
echo -e "\033[31;40m\033[4;62Hwrong,try again!\033[37;40m"
fi
done
fi
else
echo -ne "\033[22;14H" ## Clear the input line characters
## Check user input characters and act as a timer
if read tmp; then
## Successful input, loop checks if the input matches a column
for ((line = 20; line <= 60; line = line + 10)); do
if [ "$tmp" == "${putchar[$line]}" ]; then
## Clear the display in that column
clear_line $line
## If matched, clear the flag
ifchar[$line]=""
echo -e "\007\033[32;40m\033[4;62H right !\033[37;40m"
numright=$numright+1
## Exit the loop
break
else
## Otherwise, always display an error until timeout
echo -e "\033[31;40m\033[4;62Hwrong,try again!\033[37;40m"
fi
done
fi
fi
trap " doexit " 2 ## Capture special signals
## Calculate accuracy
accuracy=$numright*100/$numtotal
done
}
Now let's explain the method of typing single characters and words separately, as they are slightly different.
For typing single characters, we want five characters to appear at the same time, and they will continue to fall at a set time interval until they disappear when they reach a certain height (determined by the timeout period chosen at the beginning). If they are not hit (the user does not input the corresponding correct character) before disappearing, a new character will appear in that column. Therefore, first, we loop to initialize a character sequence and store it in an array. The index of the array represents the column number of the terminal, which has the advantage of independently managing each column of characters, whether it has timed out or been hit, and where it should be placed. The disadvantage is that we will create a relatively large array, but it will not be so large that it cannot be handled. This doesn't matter for the shell since it doesn't allocate memory based on the size of the array index. The first for loop is responsible for this, and it also checks at the end of each major loop if a column is empty and then a new character will appear in that column.
Next is a long if...else statement. Do you remember the parameter passed when calling the main function? The outermost part is used to distinguish whether to type single characters or words. Let's first look at typing single characters, which contains a crucial line:
if read -n 1 -t 0.5 tmp
The -n parameter of the read command specifies the length of the characters to be read. Here, a length of 1 is specified, which means that the input ends immediately after the user enters one character, without the need for pressing the Enter key. The -t parameter specifies the input timeout time. If the user does not input or the input is not completed within the timeout period, the reading will end immediately. Because the read command is a synchronous operation for the same shell when reading user input, the implementation of the character falling relies on this timeout (indirectly implementing a delay in falling). Here, 0.5s is set because most users can complete the input of one character within 0.5s. After reading the user input, it uses a for loop to compare each character array corresponding to each column, and if there is a match, it calls a clear_line function to clear the character in the current column, and sets the flag variable ifchar[$line] to 0 which indicates it has been cleared.
The flow of typing words is basically the same as typing characters, except that because we cannot estimate the time it takes for the user to input a word with an indefinite length, we cannot set the input timeout time for the read command. Without this timeout, the final implementation of typing words may not automatically fall like when typing characters, but instead, the word falls one line after we enter a word and press Enter. Of course, you can consider using other methods to achieve the same or better effects as typing characters.
In addition, the handling of obtaining new words for each column is slightly different because we are reading from a file, rather than generating random words. Do you still remember the file descriptor 4 we created earlier, which points to a word file? We used this file descriptor here:
read -u 4 word
if [ "$word" == "" ]; then ## End of file reading
exec 4< $file
fi
putchar[$line]=$word
Here, we still use the read command, and with the -u parameter, we can specify from which file descriptor to read the file line by line into the variable. The subsequent empty check statement is to recreate the file descriptor that points to the file when a file reaches its end, so that read can read from the beginning of the file again.
Do you feel that it's a bit complicated? Don't worry, we're not done yet. Next, pay attention to the penultimate line of the main function:
trap " doexit " 2 ## Capture special signals
The trap command is used to capture special signals in the shell, such as Ctrl+C, Ctrl+D, Ctrl+Z, ESC, etc. Originally, these special signals are handled by the shell itself. Because we want the game to exit more gracefully when it exits, we can intercept the 2nd special signal, which is Ctrl+C, to implement some customized processing. For example, here, after capturing the 2nd signal, it calls the doexit function below to exit the program gracefully:
function doexit() {
draw_border
echo -e "\033[10;30Hthis game will exit....."
echo -e "\033[0m"
sleep 2
clear
exit 1
}
Main Code Flow
Since the code for each function module is already in place, to make the program run, we still need a main code flow to call these functions.
draw_border
dis_welcome
echo -ne "\033[3;30Hstart the game. Y/N : "
read yourchoice
if [ "$yourchoice" == "Y" ] || [ "$yourchoice" == "y" ]; then
draw_border
modechoose
else
clear
exit 1
fi
exit 1
This piece of code creates an entry point for a character falling game. It first displays a welcome screen and a prompt to start the game, then waits for user input on whether to start the game. If the user chooses to start the game, they will be prompted to choose the difficulty mode of the game. If the user chooses to quit or enters an invalid input, the game will not start.
Running and Testing
Next, we can run our shell typing game:
cd ~/project
bash shell_typing_game.sh

Summary
You have just created a project to build a simple typing game in the shell. You can follow these steps to create your own typing game project and choose different game modes and difficulty levels. This project provides practical experience in shell scripting and terminal-based game development. If you are not familiar with some of the commands used in the course, such as the various outputs of echo, the I/O redirection of exec, and the trap for capturing special signals, you can practice them more to become familiar with their usage.



