Créer un jeu de frappe avec Bash

LinuxLinuxBeginner
Pratiquer maintenant

💡 Ce tutoriel est traduit par l'IA à partir de la version anglaise. Pour voir la version originale, vous pouvez cliquer ici

Introduction

Dans ce projet, vous apprendrez à créer un simple jeu de frappe à l'aide d'un script shell. Le jeu affiche des caractères aléatoires à l'écran, et votre objectif est de les taper avant qu'ils ne disparaissent. Le jeu propose différents modes avec des niveaux de difficulté variables. Vous pouvez choisir de pratiquer la frappe de nombres, de lettres, d'un mélange des deux, ou même de vos propres mots personnalisés.

👀 Aperçu

Shell Typing Game

🎯 Tâches

Dans ce projet, vous apprendrez :

  • Comment créer un fichier de projet et l'ouvrir dans un éditeur de code
  • Comment afficher une interface de bienvenue en utilisant des caractères spéciaux et des couleurs
  • Comment implémenter un menu de sélection de mode pour choisir le niveau de difficulté
  • Comment implémenter un menu de sélection de catégorie de frappe pour choisir le type de caractères à pratiquer
  • Comment créer des fonctions pour dessiner une bordure et remplir la couleur de fond de l'interface de frappe
  • Comment générer des lettres et des nombres aléatoires pour le jeu de frappe
  • Comment implémenter la fonctionnalité de frappe, y compris la gestion de l'entrée utilisateur et le calcul de la précision
  • Comment créer une fonction de sortie gracieuse pour gérer les signaux spéciaux

🏆 Réalisations

Après avoir terminé ce projet, vous serez en mesure de :

  • Démontrez les bases de la programmation shell
  • Utilisez des caractères spéciaux et des couleurs dans la sortie du terminal
  • Lisez l'entrée de l'utilisateur dans les scripts shell
  • Implémentez des menus et des interfaces utilisateur dans les scripts shell
  • Gérez les signaux spéciaux dans les scripts shell

Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL linux(("Linux")) -.-> linux/BasicFileOperationsGroup(["Basic File Operations"]) linux(("Linux")) -.-> linux/TextProcessingGroup(["Text Processing"]) linux(("Linux")) -.-> linux/VersionControlandTextEditorsGroup(["Version Control and Text Editors"]) linux(("Linux")) -.-> linux/BasicSystemCommandsGroup(["Basic System Commands"]) linux(("Linux")) -.-> linux/FileandDirectoryManagementGroup(["File and Directory Management"]) 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{{"Créer un jeu de frappe avec Bash"}} linux/clear -.-> lab-298847{{"Créer un jeu de frappe avec Bash"}} linux/read -.-> lab-298847{{"Créer un jeu de frappe avec Bash"}} linux/printf -.-> lab-298847{{"Créer un jeu de frappe avec Bash"}} linux/declare -.-> lab-298847{{"Créer un jeu de frappe avec Bash"}} linux/touch -.-> lab-298847{{"Créer un jeu de frappe avec Bash"}} linux/cd -.-> lab-298847{{"Créer un jeu de frappe avec Bash"}} linux/awk -.-> lab-298847{{"Créer un jeu de frappe avec Bash"}} linux/gedit -.-> lab-298847{{"Créer un jeu de frappe avec Bash"}} end

Créer le fichier de projet

Pour commencer, veuillez créer un nouveau fichier nommé shell_typing_game.sh et l'ouvrir dans votre éditeur de code préféré.

cd ~/project
touch shell_typing_game.sh
✨ Vérifier la solution et pratiquer

Afficher l'interface de bienvenue

#!/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   ## Ligne de départ de l'impression.
  line_char_count=65 ## Définit la position du saut de ligne, 64 caractères par ligne plus les sauts de ligne, soit un total de 65 caractères.

  echo -ne "\033[37;40m\033[5;3H" ## Définit la couleur et la position de départ du curseur.

  for ((i = 0; i < ${#str}; i++)); do
    ## Saut de ligne.
    if [ "$((i % line_char_count))" == "0" ] && [ "$i"!= "0" ]; then
      row=$row+1
      echo -ne "\033["$row";3H"
    fi
    ## Détermine les caractères de premier plan et d'arrière-plan.
    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 longue chaîne au début 010101... est l'interface de bienvenue du jeu à afficher. Si vous la regardez directement, vous risquez de ne pas comprendre ce que c'est. Vous pouvez donc copier cette chaîne dans l'éditeur gedit, puis appuyer sur le raccourci Ctrl+F pour effectuer une recherche. Lorsque vous entrez 1, vous comprendrez immédiatement. Concentrons-nous sur la commande echo. Nous devons utiliser la commande echo car nous voulons l'afficher à l'écran (ici, il s'agit du terminal Xfce. L'entrée standard, la sortie standard et la sortie d'erreur standard sous Linux sont toutes connectées au terminal par défaut). Pour réaliser certaines fonctions spéciales, telles que l'impression à une position spécifiée, la définition de la couleur des caractères affichés et l'impression de l'arrière-plan de l'ensemble du jeu, nous devons utiliser certains paramètres spéciaux de la commande correspondante, comme indiqué dans le code suivant :

## Définit la couleur et la position de départ du curseur.
echo -ne "\033[37;40m\033[5;3H"

D'après les commentaires, nous pouvons à peu près comprendre sa fonction. Mais que signifie-t-il exactement? Tout d'abord, le paramètre -n signifie d'imprimer sur la ligne actuelle sans saut de ligne après la sortie. Ensuite, définissez la couleur des caractères de sortie et la position du curseur. Parmi eux, \033[couleur de premier plan;couleur d'arrière-plan m, et ensuite \033[ligne;colonne H. Cependant, pour que echo reconnaisse ces séquences d'échappement spéciales, nous avons besoin du paramètre -e, sinon echo affichera ces caractères tels quels.

Couleurs de premier plan : 30 - 39, couleurs d'arrière-plan : 40 - 49.

Valeur Couleurs de premier plan Valeur Plage de couleurs d'arrière-plan : 40 - 49
30 Définit le premier plan en noir 40 Définit l'arrière-plan en noir
31 Définit le premier plan en rouge 41 Définit l'arrière-plan en rouge
32 Définit le premier plan en vert 42 Définit l'arrière-plan en vert
33 Définit le premier plan en marron 43 Définit l'arrière-plan en marron
34 Définit le premier plan en bleu 44 Définit l'arrière-plan en bleu
35 Définit le premier plan en violet 45 Définit l'arrière-plan en violet
36 Définit le premier plan en cyan 46 Définit l'arrière-plan en cyan
37 Définit le premier plan en blanc 47 Définit l'arrière-plan en blanc
38 Active le soulignement sur la couleur de premier plan par défaut 49 Définit l'arrière-plan en noir par défaut
39 Désactive le soulignement sur la couleur de premier plan par défaut

Qui aurait pensé que echo pouvait être utilisé de cette manière! Vous avez l'impression de le découvrir à nouveau? Maintenant, testons ce morceau de code dans le terminal.

cd ~/project
bash shell_typing_game.sh
Shell Typing Game
✨ Vérifier la solution et pratiquer
## Déclare la variable 'time' pour représenter le délai d'attente de frappe, où différents niveaux de difficulté correspondent à différentes valeurs de délai
declare -i time
function modechoose() {
  ## Sélectionner parmi trois modes différents
  echo -e "\033[8;30H1) Mode facile"
  echo -e "\033[9;30H2) Mode normal"
  echo -e "\033[10;30H3) Mode difficile"
  echo -ne "\033[22;2HVeuillez entrer votre choix : "
  read mode
  case $mode in
    "1")
      time=10
      dismenu ## Appelle la fonction de sélection de menu
      ;;
    "2")
      time=5
      dismenu
      ;;
    "3")
      time=3
      dismenu
      ;;
    *)
      echo -e "\033[22;2HVotre choix est incorrect, veuillez réessayer"
      sleep 1
      ;;
  esac
}

Avant de définir cette fonction, nous avons d'abord déclaré une variable entière 'time' en utilisant la commande declare -i. Ensuite, dans la déclaration case suivante, nous avons défini différentes valeurs pour la variable 'time' en fonction de la sélection de difficulté de l'utilisateur. Plus la difficulté est élevée, plus la valeur est petite. Cette variable est en fait utilisée dans la fonction de traitement de frappe ultérieure. Vous vous souvenez peut-être que dans les scripts shell, les variables déclarées ou définies à l'intérieur ou à l'extérieur d'une fonction sont considérées comme des variables globales avec une portée dans tout le fichier de script, à moins que le mot-clé 'local' soit utilisé pour déclarer la variable à l'intérieur d'une fonction. Quant à l'effet d'affichage du menu, nous utilisons toujours la commande echo, puis nous utilisons la commande read pour lire l'entrée de l'utilisateur dans la variable 'mode'.

Shell Typing Game
✨ Vérifier la solution et pratiquer
function display_menu() {
  while [ 1 ]; do
    draw_border
    echo -e "\033[8;30H1) Practiquer la frappe de nombres"
    echo -e "\033[9;30H2) Practiquer la frappe de lettres"
    echo -e "\033[10;30H3) Practiquer la frappe de caractères alphanumériques"
    echo -e "\033[11;30H4) Practiquer la frappe de mots"
    echo -e "\033[12;30H5) Quitter"
    echo -ne "\033[22;2HVeuillez entrer votre choix : "
    read choice
    case $choice in
      "1")
        draw_border
        ## Les deux suivants sont des paramètres de fonction, le premier paramètre indique le type de frappe, et le deuxième est la fonction pour déplacer les caractères
        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 "Quel fichier souhaitez-vous utiliser pour la pratique de frappe : " file
        if [! -f "$file" ]; then
          display_menu
        else
          exec 4< $file ## Créer un pipeline de fichier
          main word
          echo -e "\033[39;49m"
        fi
        ;;
      "5" | "q" | "Q")
        draw_border
        echo -e "\033[10;25HVous allez quitter ce jeu maintenant"
        echo -e "\033[39;49m"
        sleep 1
        clear
        exit 1
        ;;
      *)
        draw_border
        echo -e "\033[22;2HVotre choix est incorrect, veuillez réessayer"
        sleep 1
        ;;
    esac
  done
}

Dans cette fonction, expliquons d'abord les deux fonctions appelées dans la branche case. Tout d'abord, la fonction draw_border est utilisée pour dessiner la bordure de l'interface de frappe, ce qui sera montré plus tard. Ensuite, la fonction main est appelée. Cette fonction main n'a pas de rôle spécial comme la fonction main dans des langages tels que Java et C, elle a simplement le nom main, qui est utilisé pour indiquer qu'elle joue un rôle majeur dans tout le programme. Vous avez peut-être remarqué qu'il y a un argument après chaque main, oui, ce sont les paramètres passés à la fonction. En shell, les paramètres ne sont pas écrits entre parenthèses immédiatement après le nom de la fonction, contrairement à de nombreux autres langages. Il convient également de noter que dans la quatrième branche de la déclaration case, c'est-à-dire ces lignes :

read -p "Quel fichier souhaitez-vous utiliser pour la pratique de frappe : " file
if [! -f "$file" ]; then
  display_menu
else
  exec 4< $file ## Créer un pipeline de fichier
  main word
  echo -e "\033[39;49m"
fi

Selon l'invite du menu, nous savons que cette branche est pour pratiquer la frappe de mots. Ce programme permet aux utilisateurs d'utiliser des fichiers de mots personnalisés (fichiers texte avec certaines exigences de formatage, un mot par ligne, vous pouvez télécharger un fichier de liste de mots en ligne et utiliser la commande awk pour extraire) pour la pratique. Donc, d'abord, nous devons lire le nom d'un fichier entré par l'utilisateur pour sélectionner le fichier de mots. Ensuite, nous utilisons la commande exec pour créer un pipeline qui pointe vers le fichier, c'est-à-dire rediriger la sortie du fichier vers le descripteur de fichier 4 (fd). Étant donné que exec est une commande intégrée dans bash, vous ne pourrez pas voir la documentation de exec en utilisant la commande man. La redirection d'E/S avec exec est généralement liée aux fd, et le shell a généralement 10 fd, allant de 0 à 9. Les fd couramment utilisés sont 3, à savoir 0 (stdin, entrée standard), 1 (stdout, sortie standard) et 2 (stderr, sortie d'erreur standard). Comprenez simplement son sens pour l'instant.

Shell Typing Game
✨ Vérifier la solution et pratiquer

Dessiner une bordure pour une interface de frappe

function draw_border() {
  declare -i width
  declare -i high
  width=79 ## Largeur par défaut du terminal - 1
  high=23  ## Hauteur par défaut du terminal - 1

  clear

  ## Définir la couleur d'affichage en blanc sur fond noir
  echo -e "\033[37;40m"
  ## Définir la couleur de fond
  for ((i = 1; i <= $width; i = i + 1)); do
    for ((j = 1; j <= $high; j = j + 1)); do
      ## Définir la position d'affichage
      echo -e "\033["$j";"$i"H "
    done
  done
  ## Dessiner la bordure de fond
  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
}
  • Dans la fonction draw_border(), deux variables entières width et high sont déclarées, qui représentent respectivement la largeur et la hauteur de la fenêtre du terminal.
  • La commande echo -e "\033[37;40m" est utilisée pour définir la couleur d'affichage. Des boucles for imbriquées sont utilisées pour parcourir chaque ligne et chaque colonne du terminal afin de remplir le fond.
  • À l'intérieur de la boucle interne, des codes d'échappement ANSI sont utilisés pour positionner le curseur aux coordonnées souhaitées dans le terminal. Par exemple, echo -e "\033["$j";"$i"H" positionne le curseur à la ligne j et à la colonne i.
  • Après avoir rempli le fond du terminal, des bordures décoratives sont dessinées en utilisant des caractères spécifiques. Les caractères utilisés incluent +, - et |, qui sont couramment utilisés pour dessiner des bordures. Les positions de ces caractères sont déterminées par leurs valeurs de ligne et de colonne, et imprimées à l'aide de codes d'échappement ANSI. Par exemple, echo -e "\033[1;1H+" place le caractère + dans le coin supérieur gauche du terminal.

En résumé, la fonction draw_border() efface le terminal, définit la couleur de fond en noir, remplit le terminal avec des espaces pour créer un fond. Enfin, elle dessine une bordure esthétiquement agréable en plaçant des caractères (tels que + pour les coins, - et | pour les lignes) à des positions spécifiques.

Shell Typing Game
✨ Vérifier la solution et pratiquer

Remplir la couleur de fond de l'interface de frappe

## Effacer toute la zone d'atterrissage des caractères
function clear_all_area() {
  local i j
  ## Remplir la zone de frappe
  for ((i = 5; i <= 21; i++)); do
    for ((j = 3; j <= 77; j = j + 1)); do
      ## Définir la position d'affichage
      echo -e "\033[44m\033["$i";"$j"H "
    done
  done
  echo -e "\033[37;40m"
}

## Fonction : Effacer une colonne spécifique de caractères
## Entrée : Le numéro de colonne à effacer
## Retour : Aucun
function clear_line() {
  local i
  ## Remplir la zone de frappe
  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 fonction clear_all_area() :
  • Elle est utilisée pour effacer toute la zone d'atterrissage des caractères. Son rôle est de remplir les caractères de la zone d'atterrissage des caractères avec la couleur de fond (noir) afin d'effacer les caractères dans la fenêtre du terminal.
  • Elle utilise des boucles imbriquées pour parcourir toutes les combinaisons de i et j, où i représente la ligne et j représente la colonne.
  • À l'intérieur de la boucle interne, elle positionne le curseur à la ligne et à la colonne spécifiées à l'aide de codes d'échappement ANSI, puis utilise echo -e "\033[44m\033["$i";"$j"H " pour imprimer un caractère espace à cette position, mais avec la couleur de fond définie sur noir (code 44).
  • Après les boucles, la fonction utilise echo -e "\033[37;40m" pour restaurer la couleur du texte et la couleur de fond à leurs valeurs par défaut (texte blanc, fond noir).
  1. La fonction clear_line() :
  • Cette fonction est utilisée pour effacer une colonne spécifique dans la zone d'atterrissage des caractères. Elle est généralement utilisée pour effacer le chemin des caractères dans cette colonne après que l'utilisateur ait entré le bon caractère.
  • La fonction prend un argument $1, qui représente le numéro de colonne à effacer.
  • Elle utilise une boucle pour parcourir les lignes et les colonnes dans la zone d'atterrissage des caractères et remplit le chemin des caractères de cette colonne avec la couleur de fond (noir).
  • De même que clear_all_area(), la fonction positionne le curseur à la ligne et à la colonne spécifiées à l'aide de codes d'échappement ANSI, puis utilise echo -e "\033[44m\033["$i";"$j"H " pour imprimer un caractère espace à cette position, mais avec la couleur de fond définie sur noir.
  • Enfin, la fonction utilise echo -e "\033[37;40m" pour restaurer la couleur du texte et la couleur de fond à leurs valeurs par défaut.
✨ Vérifier la solution et pratiquer

Générer des lettres et des nombres aléatoires

## Fonction : Déplacer le caractère le long du chemin de chute.
## Entrée :   Paramètre 1 : La ligne actuelle du caractère (liée à la durée écoulée depuis le caractère).
##            Paramètre 2 : La colonne actuelle du caractère.
##            Paramètre 3 : (Paramètre non utilisé).
## Retour :   Aucun
function move() {

  local locate_row lastloca
  locate_row=$(($1 + 5))
  ## Afficher le caractère à saisir.
  echo -e "\033[30;44m\033["$locate_row";"$2"H$3\033[37;40m"
  if [ "$1" -gt "0" ]; then
    lastloca=$(($locate_row - 1))
    ## Effacer la position précédente.
    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
}

## Fonction : Générer un caractère aléatoire du type correspondant pour être converti en un caractère aléatoire.
## Entrée :   Le type de caractère à générer.
## Variable globale : random_char, array[]
## Retour :   Aucun
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]}
}

Étant donné que les variables de chaîne de caractères en shell ne prennent pas directement en charge l'indexation, nous devons placer toutes les lettres et les nombres dans un tableau indexé. C'est le but de la fonction putarray. La fonction get_random_char ci-dessous est utilisée pour générer des lettres et des nombres aléatoires. Elle utilise la variable d'environnement de nombre aléatoire du système RANDOM pour obtenir un index aléatoire, puis lit le caractère correspondant dans le tableau.

✨ Vérifier la solution et pratiquer

Implémentation de la frappe

Après toutes les préparations, nous pouvons enfin commencer à implémenter la fonction de frappe. Jetons un coup d'œil au code suivant :

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

  ## Stocker les caractères correspondants dans un tableau, $1 représente le type de caractère sélectionné par l'utilisateur
  putarray $1

  ## Initialiser le temps de début du jeu
  gamestarttime=$(date +%s)

  while [ 1 ]; do
    echo -e "\033[2;2H  Veuillez entrer la lettre à l'écran avant qu'elle ne disparaisse!"

    echo -e "\033[3;2H Temps de jeu :     "
    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 Précision : \033[31;40m$accuracy %\033[37;40m"
    echo -ne "\033[22;2H Votre saisie :                         "
    clear_all_area
    ## Boucler 10 fois pour vérifier si une colonne de caractères a expiré ou a été touchée
    for ((line = 20; line <= 60; line = line + 10)); do
      ## Vérifier si la colonne de caractères a été touchée
      if [ "${ifchar[$line]}" == "" ] || [ "${donetime[$line]}" -gt "$time" ]; then
        ## Effacer l'affichage dans cette colonne
        clear_line $line
        ## Générer un caractère aléatoire
        if [ "$1" == "word" ]; then
          read -u 4 word
          if [ "$word" == "" ]; then ## Fin de la lecture du fichier
            exec 4< $file
          fi
          putchar[$line]=$word
        else
          get_random_char $1
          putchar[$line]=$random_char
        fi
        numtotal=$numtotal+1
        ## Définir le drapeau à 1
        ifchar[$line]=1
        ## Réinitialiser le minuteur
        starttime[$line]=$(date +%s)
        curtime[$line]=${starttime[$line]}
        donetime[$line]=$time
        ## Réinitialiser la position de la colonne à 0
        column[$line]=0
        if [ "$1" == "word" ]; then
          move 0 $line ${putchar[$line]}
        fi
      else
        ## Si elle n'a pas expiré ou été touchée, mettre à jour le minuteur et la position actuelle
        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" ## Effacer les caractères de la ligne de saisie
      ## Vérifier les caractères saisis par l'utilisateur et agir comme un minuteur d'une seconde
      if read -n 1 -t 0.5 tmp; then
        ## Saisie réussie, boucle pour vérifier si la saisie correspond à une colonne
        for ((line = 20; line <= 60; line = line + 10)); do
          if [ "$tmp" == "${putchar[$line]}" ]; then
            ## Effacer l'affichage dans cette colonne
            clear_line $line
            ## Si correspondance, effacer le drapeau
            ifchar[$line]=""
            echo -e "\007\033[32;40m\033[4;62H         correct!\033[37;40m"
            numright=$numright+1
            ## Sortir de la boucle
            break
          else
            ## Sinon, afficher toujours une erreur jusqu'à l'expiration
            echo -e "\033[31;40m\033[4;62Hfaux, réessayez!\033[37;40m"
          fi
        done
      fi
    else
      echo -ne "\033[22;14H" ## Effacer les caractères de la ligne de saisie
      ## Vérifier les caractères saisis par l'utilisateur et agir comme un minuteur
      if read tmp; then
        ## Saisie réussie, boucle pour vérifier si la saisie correspond à une colonne
        for ((line = 20; line <= 60; line = line + 10)); do
          if [ "$tmp" == "${putchar[$line]}" ]; then
            ## Effacer l'affichage dans cette colonne
            clear_line $line
            ## Si correspondance, effacer le drapeau
            ifchar[$line]=""
            echo -e "\007\033[32;40m\033[4;62H         correct!\033[37;40m"
            numright=$numright+1
            ## Sortir de la boucle
            break
          else
            ## Sinon, afficher toujours une erreur jusqu'à l'expiration
            echo -e "\033[31;40m\033[4;62Hfaux, réessayez!\033[37;40m"
          fi
        done
      fi
    fi
    trap " doexit " 2 ## Capturer des signaux spéciaux
    ## Calculer la précision
    accuracy=$numright*100/$numtotal
  done
}

Maintenant, expliquons séparément la méthode de frappe de caractères simples et de mots, car elles sont légèrement différentes.

Pour la frappe de caractères simples, nous voulons que cinq caractères apparaissent en même temps, et qu'ils continuent de tomber à un intervalle de temps défini jusqu'à ce qu'ils disparaissent lorsqu'ils atteignent une certaine hauteur (déterminée par la période d'expiration choisie au début). Si ils ne sont pas touchés (l'utilisateur n'entre pas le bon caractère correspondant) avant de disparaître, un nouveau caractère apparaîtra dans cette colonne. Par conséquent, tout d'abord, nous bouclons pour initialiser une séquence de caractères et la stockons dans un tableau. L'index du tableau représente le numéro de colonne du terminal, ce qui a l'avantage de gérer indépendamment chaque colonne de caractères, si elle a expiré ou a été touchée, et où elle devrait être placée. L'inconvénient est que nous allons créer un tableau relativement grand, mais il ne sera pas si grand qu'il ne puisse être géré. Cela ne pose pas de problème pour le shell car il n'alloue pas de mémoire en fonction de la taille de l'index du tableau. La première boucle for est responsable de cela, et elle vérifie également à la fin de chaque grande boucle si une colonne est vide, puis un nouveau caractère apparaîtra dans cette colonne.

Ensuite, il y a une longue instruction if...else. Vous vous souvenez du paramètre passé lors de l'appel de la fonction main? La partie la plus extérieure est utilisée pour distinguer si l'on frappe des caractères simples ou des mots. Regardons d'abord la frappe de caractères simples, qui contient une ligne cruciale :

if read -n 1 -t 0.5 tmp

Le paramètre -n de la commande read spécifie la longueur des caractères à lire. Ici, une longueur de 1 est spécifiée, ce qui signifie que la saisie se termine immédiatement après que l'utilisateur a entré un caractère, sans avoir besoin d'appuyer sur la touche Entrée. Le paramètre -t spécifie le temps d'expiration de la saisie. Si l'utilisateur n'entre rien ou si la saisie n'est pas terminée dans la période d'expiration, la lecture se terminera immédiatement. Parce que la commande read est une opération synchrone pour le même shell lors de la lecture de la saisie de l'utilisateur, l'implémentation de la chute des caractères repose sur cet expiration (en implémentant indirectement un délai de chute). Ici, 0,5s est défini car la plupart des utilisateurs peuvent terminer la saisie d'un caractère en moins de 0,5s. Après avoir lu la saisie de l'utilisateur, il utilise une boucle for pour comparer chaque tableau de caractères correspondant à chaque colonne, et s'il y a une correspondance, il appelle une fonction clear_line pour effacer le caractère dans la colonne actuelle, et définit la variable de drapeau ifchar[$line] sur 0 ce qui indique qu'il a été effacé.

Le flux de frappe de mots est essentiellement le même que celui de la frappe de caractères, sauf que parce que nous ne pouvons pas estimer le temps qu'il faut à l'utilisateur pour saisir un mot de longueur indéfinie, nous ne pouvons pas définir le temps d'expiration de la saisie pour la commande read. Sans cet expiration, la mise en œuvre finale de la frappe de mots peut ne pas tomber automatiquement comme lors de la frappe de caractères, mais plutôt que le mot tombe d'une ligne après que nous ayons saisi un mot et appuyé sur Entrée. Bien sûr, vous pouvez envisager d'utiliser d'autres méthodes pour obtenir les mêmes ou de meilleurs résultats que la frappe de caractères.

De plus, la gestion de l'obtention de nouveaux mots pour chaque colonne est légèrement différente car nous lisons à partir d'un fichier, plutôt que de générer des mots aléatoires. Vous vous souvenez toujours du descripteur de fichier 4 que nous avons créé plus tôt, qui pointe vers un fichier de mots? Nous avons utilisé ce descripteur de fichier ici :

read -u 4 word
if [ "$word" == "" ]; then ## Fin de la lecture du fichier
  exec 4< $file
fi
putchar[$line]=$word

Ici, nous utilisons toujours la commande read, et avec le paramètre -u, nous pouvons spécifier à partir de quel descripteur de fichier lire le fichier ligne par ligne dans la variable. La déclaration de vérification de vide suivante est pour recréer le descripteur de fichier qui pointe vers le fichier lorsqu'un fichier atteint sa fin, afin que read puisse lire à partir du début du fichier à nouveau.

Vous trouvez cela un peu compliqué? Ne vous inquiétez pas, nous n'avons pas fini. Ensuite, faites attention à l'avant-dernière ligne de la fonction main :

trap " doexit " 2 ## Capturer des signaux spéciaux

La commande trap est utilisée pour capturer des signaux spéciaux dans le shell, tels que Ctrl+C, Ctrl+D, Ctrl+Z, ESC, etc. À l'origine, ces signaux spéciaux sont gérés par le shell lui-même. Parce que nous voulons que le jeu quitte plus gracieusement lorsqu'il se termine, nous pouvons intercepter le 2ème signal spécial, qui est Ctrl+C, pour implémenter certains traitements personnalisés. Par exemple, ici, après avoir capturé le 2ème signal, il appelle la fonction doexit ci-dessous pour quitter le programme gracieusement :

function doexit() {
  draw_border
  echo -e "\033[10;30Hce jeu va se terminer....."
  echo -e "\033[0m"
  sleep 2
  clear
  exit 1
}
✨ Vérifier la solution et pratiquer

Flux principal du code

Étant donné que le code de chaque module de fonction est déjà en place, pour faire fonctionner le programme, nous avons encore besoin d'un flux principal de code pour appeler ces fonctions.

draw_border
dis_welcome
echo -ne "\033[3;30HCommencer le jeu. O/N : "
read yourchoice
if [ "$yourchoice" == "O" ] || [ "$yourchoice" == "o" ]; then
  draw_border
  modechoose
else
  clear
  exit 1
fi

exit 1

Ce morceau de code crée un point d'entrée pour un jeu de caractères tombants. Il affiche d'abord un écran de bienvenue et une invite pour commencer le jeu, puis attend la saisie de l'utilisateur pour savoir s'il souhaite commencer le jeu. Si l'utilisateur choisit de commencer le jeu, il sera invité à choisir le mode de difficulté du jeu. Si l'utilisateur choisit de quitter ou entre une saisie invalide, le jeu ne démarrera pas.

✨ Vérifier la solution et pratiquer

Exécution et test

Ensuite, nous pouvons exécuter notre jeu de frappe en shell :

cd ~/project
bash shell_typing_game.sh
Jeu de frappe en shell
✨ Vérifier la solution et pratiquer

Résumé

Vous venez de créer un projet pour construire un simple jeu de frappe dans le shell. Vous pouvez suivre ces étapes pour créer votre propre projet de jeu de frappe et choisir différents modes de jeu et niveaux de difficulté. Ce projet offre une expérience pratique en script shell et en développement de jeux basés sur le terminal. Si vous n'êtes pas familier avec certaines des commandes utilisées dans le cours, telles que les diverses sorties de echo, la redirection d'E/S de exec et la capture de signaux spéciaux avec trap, vous pouvez les pratiquer davantage pour vous familiariser avec leur utilisation.