Crear un juego de escritura utilizando Bash

LinuxLinuxBeginner
Practicar Ahora

💡 Este tutorial está traducido por IA desde la versión en inglés. Para ver la versión original, puedes hacer clic aquí

Introducción

En este proyecto, aprenderás cómo crear un sencillo juego de escritura a máquina utilizando un script de shell. El juego muestra caracteres aleatorios en la pantalla, y tu objetivo es escribirlos antes de que desaparezcan. El juego ofrece diferentes modos con niveles de dificultad variables. Puedes elegir practicar la escritura de números, letras, una mezcla de ambos, o incluso tus propias palabras personalizadas.

👀 Vista previa

Shell Typing Game

🎯 Tareas

En este proyecto, aprenderás:

  • Cómo crear un archivo de proyecto y abrirlo en un editor de código
  • Cómo mostrar una interfaz de bienvenida utilizando caracteres especiales y colores
  • Cómo implementar un menú de selección de modo para elegir el nivel de dificultad
  • Cómo implementar un menú de selección de categoría de escritura para elegir el tipo de caracteres a practicar
  • Cómo crear funciones para dibujar un borde y rellenar el color de fondo de la interfaz de escritura
  • Cómo generar letras y números aleatorios para el juego de escritura
  • Cómo implementar la funcionalidad de escritura, incluyendo el manejo de la entrada del usuario y el cálculo de la precisión
  • Cómo crear una función de salida elegante para manejar señales especiales

🏆 Logros

Después de completar este proyecto, podrás:

  • Demostrar los conceptos básicos de scripting de shell
  • Utilizar caracteres especiales y colores en la salida del terminal
  • Leer la entrada del usuario en scripts de shell
  • Implementar menús e interfaces de usuario en scripts de shell
  • Manejar señales especiales en scripts de shell

Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL linux(("Linux")) -.-> linux/BasicFileOperationsGroup(["Basic File Operations"]) linux(("Linux")) -.-> linux/FileandDirectoryManagementGroup(["File and Directory Management"]) linux(("Linux")) -.-> linux/VersionControlandTextEditorsGroup(["Version Control and Text Editors"]) linux(("Linux")) -.-> linux/BasicSystemCommandsGroup(["Basic System Commands"]) linux(("Linux")) -.-> linux/TextProcessingGroup(["Text Processing"]) linux/BasicSystemCommandsGroup -.-> linux/echo("Text Display") linux/BasicSystemCommandsGroup -.-> linux/clear("Screen Clearing") linux/BasicSystemCommandsGroup -.-> linux/read("Input Reading") linux/BasicSystemCommandsGroup -.-> linux/printf("Text Formatting") linux/BasicSystemCommandsGroup -.-> linux/declare("Variable Declaring") linux/BasicFileOperationsGroup -.-> linux/touch("File Creating/Updating") linux/FileandDirectoryManagementGroup -.-> linux/cd("Directory Changing") linux/TextProcessingGroup -.-> linux/awk("Text Processing") linux/VersionControlandTextEditorsGroup -.-> linux/gedit("Graphical Text Editing") subgraph Lab Skills linux/echo -.-> lab-298847{{"Crear un juego de escritura utilizando Bash"}} linux/clear -.-> lab-298847{{"Crear un juego de escritura utilizando Bash"}} linux/read -.-> lab-298847{{"Crear un juego de escritura utilizando Bash"}} linux/printf -.-> lab-298847{{"Crear un juego de escritura utilizando Bash"}} linux/declare -.-> lab-298847{{"Crear un juego de escritura utilizando Bash"}} linux/touch -.-> lab-298847{{"Crear un juego de escritura utilizando Bash"}} linux/cd -.-> lab-298847{{"Crear un juego de escritura utilizando Bash"}} linux/awk -.-> lab-298847{{"Crear un juego de escritura utilizando Bash"}} linux/gedit -.-> lab-298847{{"Crear un juego de escritura utilizando Bash"}} end

Crear archivo de proyecto

Para comenzar, crea un nuevo archivo llamado shell_typing_game.sh y ábrelo en tu editor de código preferido.

cd ~/project
touch shell_typing_game.sh
✨ Revisar Solución y Practicar

Mostrar interfaz de bienvenida

#!/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

La larga cadena al principio 010101... es la interfaz de bienvenida del juego que se mostrará. Si la miras directamente, es posible que no entiendas de qué se trata. Así que puedes copiar esta cadena en un editor gedit y luego presionar el atajo Ctrl+F para buscar. Cuando ingreses 1, lo entenderás inmediatamente. Centrémonos en el comando echo. Necesitamos usar el comando echo porque queremos mostrarlo en la pantalla (aquí nos referimos al terminal Xfce. La entrada estándar, la salida estándar y la salida de error estándar en Linux están conectadas al terminal por defecto). Para lograr algunas funciones especiales, como imprimir en una posición específica y establecer el color de los caracteres de visualización, e imprimir el fondo de todo el juego, necesitamos usar algunos parámetros especiales del comando correspondiente, como se muestra en el siguiente código:

## Set the color and cursor start position.
echo -ne "\033[37;40m\033[5;3H"

Basándonos en los comentarios, podemos entender aproximadamente su función. Pero, ¿qué significa exactamente? En primer lugar, el parámetro -n significa imprimir en la línea actual sin salto de línea después de la salida. Luego se establece el color del carácter de salida y la posición del cursor. Entre ellos, \033[color de primer plano;color de fondo m, y lo siguiente es \033[fila;columna H. Sin embargo, para que echo reconozca estas secuencias de escape especiales, necesitamos el parámetro -e, de lo contrario echo mostrará estos caracteres tal como están.

Colores de primer plano: 30 - 39, colores de fondo: 40 - 49.

Valor Colores de primer plano Valor Rango de colores de fondo: 40 - 49
30 Establece el primer plano en negro 40 Establece el fondo en negro
31 Establece el primer plano en rojo 41 Establece el fondo en rojo
32 Establece el primer plano en verde 42 Establece el fondo en verde
33 Establece el primer plano en marrón 43 Establece el fondo en marrón
34 Establece el primer plano en azul 44 Establece el fondo en azul
35 Establece el primer plano en púrpura 45 Establece el fondo en púrpura
36 Establece el primer plano en cian 46 Establece el fondo en cian
37 Establece el primer plano en blanco 47 Establece el fondo en blanco
38 Establece subrayado en el color de primer plano predeterminado 49 Establece el fondo negro predeterminado
39 Quita el subrayado del color de primer plano predeterminado

¡No esperabas que echo se pudiera usar de esta manera! ¿Sientes que lo has conocido de nuevo? Bueno, ahora probemos este fragmento de código en el terminal.

cd ~/project
bash shell_typing_game.sh
Shell Typing Game
✨ Revisar Solución y Practicar

Mostrar menú de selección de modo

## 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
}

Antes de definir esta función, primero declaramos una variable entera 'time' utilizando el comando declare -i. Luego, en la siguiente declaración case, establecemos diferentes valores para la variable 'time' según la selección de dificultad del usuario. Cuanto mayor sea la dificultad, menor será el valor. Esta variable se utiliza en realidad en la función de procesamiento de escritura posterior. Quizás recuerdes que en los scripts de shell, las variables declaradas o definidas dentro o fuera de una función se tratan como variables globales con un alcance en todo el archivo de script, a menos que se utilice la palabra clave 'local' para declarar la variable dentro de una función. En cuanto al efecto de visualización del menú, seguimos utilizando el comando echo y luego utilizamos el comando read para leer la entrada del usuario en la variable 'mode'.

Shell Typing Game
✨ Revisar Solución y Practicar

Mostrar menú de selección de categoría de escritura

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
}

En esta función, primero explicaremos las dos funciones llamadas en la rama case. En primer lugar, la función draw_border se utiliza para dibujar el borde de la interfaz de escritura, que se mostrará más adelante. Luego, se llama a la función main. Esta función main no tiene ningún papel especial como la función main en lenguajes como Java y C, simplemente tiene el nombre main, que se utiliza para indicar que juega un papel importante en todo el programa. Puedes haber notado que hay un argumento después de cada main, sí, este es el parámetro pasado a la función. En shell, los parámetros no se escriben dentro de paréntesis inmediatamente después del nombre de la función, a diferencia de muchos otros lenguajes. También se debe tener en cuenta que en la cuarta rama de la declaración case, que son estas líneas:

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

De acuerdo con la solicitud del menú, podemos saber que esta rama es para practicar la escritura de palabras. Este programa permite a los usuarios utilizar archivos de palabras personalizados (archivos de texto con ciertos requisitos de formato, con una palabra por línea, se puede descargar una lista de palabras en línea y utilizar el comando awk para extraer) para la práctica. Entonces, primero necesitamos leer el nombre de un archivo de entrada del usuario para seleccionar el archivo de palabras. Luego, utilizamos el comando exec para crear una tubería que apunta al archivo, es decir, redirigiendo la salida del archivo al descriptor de archivo 4 (fd). Dado que exec es un comando incorporado en bash, no podrás ver la documentación de exec utilizando el comando man. La redirección de E/S con exec suele estar relacionada con los fd, y la shell suele tener 10 fd, que van del 0 al 9. Los fd más comunes son 3, que son 0 (stdin, entrada estándar), 1 (stdout, salida estándar) y 2 (stderr, salida de error estándar). Por ahora, solo entiende su significado.

Shell Typing Game
✨ Revisar Solución y Practicar

Dibujar un borde para una interfaz de escritura

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
}
  • En la función draw_border(), se declaran dos variables enteras width y high, que representan el ancho y la altura de la ventana del terminal respectivamente.
  • El comando echo -e "\033[37;40m" se utiliza para establecer el color de visualización. Se utilizan bucles for anidados para recorrer cada fila y columna del terminal y llenar el fondo.
  • Dentro del bucle interno, se utilizan códigos de escape ANSI para posicionar el cursor en las coordenadas deseadas dentro del terminal. Por ejemplo, echo -e "\033["$j";"$i"H" establece la posición del cursor en la fila j y la columna i.
  • Después de llenar el fondo del terminal, se dibujan bordes decorativos utilizando caracteres específicos. Los caracteres utilizados incluyen +, - y |, que se utilizan comúnmente para dibujar bordes. Las posiciones de estos caracteres se determinan por sus valores de fila y columna, y se imprimen utilizando códigos de escape ANSI. Por ejemplo, echo -e "\033[1;1H+" coloca el carácter + en la esquina superior izquierda del terminal.

En resumen, la función draw_border() borra el terminal, establece el color de fondo en negro, llena el terminal con espacios para crear un fondo. Finalmente, dibuja un borde visualmente atractivo colocando caracteres (como + para las esquinas, - y | para las líneas) en posiciones específicas.

Shell Typing Game
✨ Revisar Solución y Practicar

Rellenar el color de fondo de la interfaz de escritura

## 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"
}
  1. La función clear_all_area():
  • Se utiliza para borrar toda el área de aterrizaje de caracteres. Su función es rellenar los caracteres en el área de aterrizaje de caracteres con el color de fondo (negro) para borrar los caracteres en la ventana del terminal.
  • Utiliza bucles anidados para iterar sobre todas las combinaciones de i y j, donde i representa la fila y j representa la columna.
  • Dentro del bucle interno, establece la posición del cursor en la fila y columna especificadas utilizando códigos de escape ANSI, y luego utiliza echo -e "\033[44m\033["$i";"$j"H " para imprimir un carácter de espacio en esa posición, pero con el color de fondo establecido en negro (código 44).
  • Después de los bucles, la función utiliza echo -e "\033[37;40m" para restaurar el color del texto y el color de fondo a sus valores predeterminados (texto blanco, fondo negro).
  1. La función clear_line():
  • Esta función se utiliza para borrar una columna específica en el área de aterrizaje de caracteres. Por lo general, se utiliza para borrar la trayectoria de caracteres en esa columna después de que el usuario ingrese el carácter correcto.
  • La función toma un argumento $1, que representa el número de columna a borrar.
  • Utiliza un bucle para iterar sobre las filas y columnas en el área de aterrizaje de caracteres y rellena la trayectoria de caracteres de esa columna con el color de fondo (negro).
  • Similar a clear_all_area(), la función establece la posición del cursor en la fila y columna especificadas utilizando códigos de escape ANSI, y luego utiliza echo -e "\033[44m\033["$i";"$j"H " para imprimir un carácter de espacio en esa posición, pero con el color de fondo establecido en negro.
  • Finalmente, la función utiliza echo -e "\033[37;40m" para restaurar el color del texto y el color de fondo a sus valores predeterminados.
✨ Revisar Solución y Practicar

Generar letras y números aleatorios

## 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]}
}

Debido a que las variables de cadena en shell no admiten directamente la indexación, necesitamos colocar todas las letras y números en una matriz indexada. Este es el propósito de la función putarray. La función get_random_char que se muestra a continuación se utiliza para generar letras y números aleatorios. Utiliza la variable de entorno de números aleatorios del sistema RANDOM para obtener un índice aleatorio y luego lee el carácter correspondiente de la matriz.

✨ Revisar Solución y Practicar

Implementación de la escritura

Después de todas las preparaciones, finalmente podemos comenzar a implementar la función de escritura. Echemos un vistazo al siguiente código:

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
}

Ahora, explicaremos el método de escritura de caracteres individuales y palabras por separado, ya que son ligeramente diferentes.

Para la escritura de caracteres individuales, queremos que aparezcan cinco caracteres al mismo tiempo, y estos seguirán cayendo a intervalos de tiempo establecidos hasta que desaparezcan cuando alcanzan una cierta altura (determinada por el período de tiempo de espera elegido al principio). Si no son acertados (el usuario no ingresa el carácter correcto correspondiente) antes de desaparecer, aparecerá un nuevo carácter en esa columna. Por lo tanto, primero, recorremos en un bucle para inicializar una secuencia de caracteres y la almacenamos en una matriz. El índice de la matriz representa el número de columna del terminal, lo cual tiene la ventaja de gestionar de forma independiente cada columna de caracteres, ya sea si ha expirado el tiempo o ha sido acertado, y dónde debe colocarse. La desventaja es que crearemos una matriz relativamente grande, pero no será tan grande que no se pueda manejar. Esto no importa para la shell ya que no asigna memoria según el tamaño del índice de la matriz. El primer bucle for se encarga de esto, y también comprueba al final de cada gran bucle si una columna está vacía y, en ese caso, aparecerá un nuevo carácter en esa columna.

A continuación, hay una larga declaración if...else. ¿Recuerdas el parámetro pasado cuando se llama a la función main? La parte más externa se utiliza para distinguir si se deben escribir caracteres individuales o palabras. Veamos primero la escritura de caracteres individuales, que contiene una línea crucial:

if read -n 1 -t 0.5 tmp

El parámetro -n del comando read especifica la longitud de los caracteres a leer. Aquí, se especifica una longitud de 1, lo que significa que la entrada finaliza inmediatamente después de que el usuario ingrese un carácter, sin necesidad de presionar la tecla Enter. El parámetro -t especifica el tiempo de espera de la entrada. Si el usuario no ingresa nada o no completa la entrada dentro del período de tiempo de espera, la lectura finalizará inmediatamente. Debido a que el comando read es una operación síncrona para la misma shell al leer la entrada del usuario, la implementación de la caída de los caracteres se basa en este tiempo de espera (implementando indirectamente un retraso en la caída). Aquí, se establece 0.5s porque la mayoría de los usuarios pueden completar la entrada de un carácter dentro de 0.5s. Después de leer la entrada del usuario, utiliza un bucle for para comparar cada matriz de caracteres correspondiente a cada columna, y si hay una coincidencia, llama a una función clear_line para borrar el carácter en la columna actual, y establece la variable de bandera ifchar[$line] en 0, lo que indica que ha sido borrado.

El flujo de escritura de palabras es básicamente el mismo que el de escritura de caracteres, excepto que, debido a que no podemos estimar el tiempo que tardará el usuario en ingresar una palabra de longitud indefinida, no podemos establecer el tiempo de espera de entrada para el comando read. Sin este tiempo de espera, la implementación final de la escritura de palabras puede no caer automáticamente como cuando se escriben caracteres, sino que la palabra cae una línea después de que ingresamos una palabra y presionamos Enter. Por supuesto, puedes considerar utilizar otros métodos para lograr los mismos o mejores efectos que la escritura de caracteres.

Además, el manejo de la obtención de nuevas palabras para cada columna es ligeramente diferente porque estamos leyendo desde un archivo, en lugar de generar palabras aleatorias. ¿Todavía recuerdas el descriptor de archivo 4 que creamos anteriormente, que apunta a un archivo de palabras? Lo utilizamos aquí:

read -u 4 word
if [ "$word" == "" ]; then ## End of file reading
  exec 4< $file
fi
putchar[$line]=$word

Aquí, todavía utilizamos el comando read, y con el parámetro -u, podemos especificar desde qué descriptor de archivo leer el archivo línea por línea en la variable. La siguiente declaración de comprobación de vacío es para recrear el descriptor de archivo que apunta al archivo cuando un archivo llega a su final, de modo que read pueda leer desde el principio del archivo nuevamente.

¿Te parece un poco complicado? No te preocupes, todavía no hemos terminado. A continuación, presta atención a la penúltima línea de la función main:

trap " doexit " 2 ## Capture special signals

El comando trap se utiliza para capturar señales especiales en la shell, como Ctrl+C, Ctrl+D, Ctrl+Z, ESC, etc. Originalmente, estas señales especiales son manejadas por la propia shell. Debido a que queremos que el juego salga de manera más elegante cuando se cierra, podemos interceptar la segunda señal especial, que es Ctrl+C, para implementar algún procesamiento personalizado. Por ejemplo, aquí, después de capturar la segunda señal, llama a la función doexit que se muestra a continuación para salir del programa de manera elegante:

function doexit() {
  draw_border
  echo -e "\033[10;30Hthis game will exit....."
  echo -e "\033[0m"
  sleep 2
  clear
  exit 1
}
✨ Revisar Solución y Practicar

Flujo principal del código

Dado que el código de cada módulo de función ya está listo, para hacer que el programa se ejecute, todavía necesitamos un flujo principal de código para llamar a estas funciones.

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

Este fragmento de código crea un punto de entrada para un juego de caída de caracteres. Primero muestra una pantalla de bienvenida y un mensaje para iniciar el juego, luego espera la entrada del usuario sobre si desea iniciar el juego. Si el usuario elige iniciar el juego, se le pedirá que elija el modo de dificultad del juego. Si el usuario elige salir o ingresa una entrada no válida, el juego no se iniciará.

✨ Revisar Solución y Practicar

Ejecución y prueba

A continuación, podemos ejecutar nuestro juego de escritura en shell:

cd ~/project
bash shell_typing_game.sh
Shell Typing Game
✨ Revisar Solución y Practicar

Resumen

Acabas de crear un proyecto para construir un juego de escritura simple en la shell. Puedes seguir estos pasos para crear tu propio proyecto de juego de escritura y elegir diferentes modos de juego y niveles de dificultad. Este proyecto ofrece experiencia práctica en scripting de shell y desarrollo de juegos basados en terminal. Si no estás familiarizado con algunos de los comandos utilizados en el curso, como las diversas salidas de echo, la redirección de E/S de exec y la captura de señales especiales con trap, puedes practicarlos más para familiarizarte con su uso.