Programmer un jeu de devinette

Intermediate

This tutorial is from open-source community. Access the source code

Introduction

Bienvenue dans Programming a Guessing Game. Ce laboratoire est une partie du Rust Book. Vous pouvez pratiquer vos compétences Rust dans LabEx.

Dans ce laboratoire, nous allons implémenter un jeu de devinette en Rust, où le programme génère un nombre aléatoire et invite le joueur à le deviner, en fournissant des informations sur le fait que la proposition est trop basse ou trop haute, et en félicitant le joueur s'ils devinent correctement.

Ceci est un Guided Lab, qui fournit des instructions étape par étape pour vous aider à apprendre et à pratiquer. Suivez attentivement les instructions pour compléter chaque étape et acquérir une expérience pratique. Les données historiques montrent que c'est un laboratoire de niveau intermédiaire avec un taux de réussite de 65%. Il a reçu un taux d'avis positifs de 100% de la part des apprenants.

Programming a Guessing Game

Plongeons ensemble dans Rust grâce à un projet pratique! Ce chapitre vous présente quelques concepts courants de Rust en vous montrant comment les utiliser dans un programme réel. Vous allez découvrir let, match, les méthodes, les fonctions associées, les boîtes externes et bien plus encore! Dans les chapitres suivants, nous explorerons ces idées en détail. Dans ce chapitre, vous allez simplement pratiquer les bases.

Nous allons implémenter un problème classique de programmation pour débutants : un jeu de devinette. Voici comment ça fonctionne : le programme générera un entier aléatoire compris entre 1 et 100. Il invitera ensuite le joueur à entrer une proposition. Après que la proposition ait été entrée, le programme indiquera si la proposition est trop basse ou trop haute. Si la proposition est correcte, le jeu imprimera un message de félicitations et sortira.

Setting Up a New Project

Pour créer un nouveau projet, accédez au répertoire project que vous avez créé au chapitre 1 et créez un nouveau projet avec Cargo, comme ceci :

cargo new guessing_game
cd guessing_game

La première commande, cargo new, prend le nom du projet (guessing_game) comme premier argument. La deuxième commande change dans le répertoire du nouveau projet.

Regardez le fichier Cargo.toml généré :

Nom du fichier : Cargo.toml

[package]
name = "guessing_game"
version = "0.1.0"
edition = "2021"

## Consultez plus de clés et leurs définitions à
https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

Comme vous l'avez vu au chapitre 1, cargo new génère un programme "Bonjour, le monde!" pour vous. Vérifiez le fichier src/main.rs :

Nom du fichier : src/main.rs

fn main() {
    println!("Hello, world!");
}

Maintenant, compilons ce programme "Bonjour, le monde!" et exécutons-le dans la même étape en utilisant la commande cargo run :

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 1.50s
     Running `target/debug/guessing_game`
Hello, world!

La commande run est pratique lorsque vous avez besoin d'itérer rapidement sur un projet, comme nous le ferons dans ce jeu, en testant rapidement chaque itération avant de passer à la suivante.

Réouvrez le fichier src/main.rs. Vous allez écrire tout le code dans ce fichier.

Processing a Guess

La première partie du programme du jeu de devinette demandera l'entrée de l'utilisateur, traitera cette entrée et vérifiera que l'entrée est dans la forme attendue. Pour commencer, nous allons permettre au joueur d'entrer une proposition. Entrez le code de la Liste 2-1 dans src/main.rs.

Nom du fichier : src/main.rs

use std::io;

fn main() {
    println!("Guess the number!");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
     .read_line(&mut guess)
     .expect("Failed to read line");

    println!("You guessed: {guess}");
}

Liste 2-1 : Code qui obtient une proposition de l'utilisateur et l'affiche

Ce code contient beaucoup d'informations, passons donc en revue chaque ligne. Pour obtenir l'entrée de l'utilisateur puis afficher le résultat en sortie, nous devons inclure la bibliothèque d'entrée/sortie io dans la portée. La bibliothèque io provient de la bibliothèque standard appelée std :

use std::io;

Par défaut, Rust a un ensemble d'éléments définis dans la bibliothèque standard qu'il inclut dans la portée de chaque programme. Cet ensemble est appelé le préambule, et vous pouvez voir tout ce qu'il contient sur https://doc.rust-lang.org/std/prelude/index.html.

Si un type que vous voulez utiliser n'est pas dans le préambule, vous devez l'inclure explicitement dans la portée avec une instruction use. L'utilisation de la bibliothèque std::io vous offre un certain nombre de fonctionnalités utiles, y compris la possibilité d'accepter l'entrée de l'utilisateur.

Comme vous l'avez vu au chapitre 1, la fonction main est le point d'entrée du programme :

fn main() {

La syntaxe fn déclare une nouvelle fonction ; les parenthèses () indiquent qu'il n'y a pas de paramètres ; et la accolade { démarre le corps de la fonction.

Comme vous l'avez également appris au chapitre 1, println! est un macro qui imprime une chaîne sur l'écran :

println!("Guess the number!");

println!("Please input your guess.");

Ce code imprime une invite indiquant ce que le jeu est et demandant une entrée à l'utilisateur.

Storing Values with Variables

Ensuite, nous allons créer une variable pour stocker l'entrée de l'utilisateur, comme ceci :

let mut guess = String::new();

Maintenant, le programme devient intéressant! Il y a beaucoup de choses qui se passent dans cette petite ligne. Nous utilisons l'instruction let pour créer la variable. Voici un autre exemple :

let apples = 5;

Cette ligne crée une nouvelle variable nommée apples et l'associe à la valeur 5. En Rust, les variables sont immuables par défaut, ce qui signifie qu'une fois que nous avons donné une valeur à la variable, la valeur ne changera pas. Nous aborderons ce concept en détail dans "Variables and Mutability". Pour rendre une variable mutable, nous ajoutons mut avant le nom de la variable :

let apples = 5; // immutable
let mut bananas = 5; // mutable

Note : La syntaxe // démarre un commentaire qui continue jusqu'à la fin de la ligne. Rust ignore tout ce qui se trouve dans les commentaires. Nous aborderons les commentaires en détail au chapitre 3.

Revenons au programme du jeu de devinette. Maintenant, vous savez que let mut guess introduira une variable mutable nommée guess. Le signe égal (=) indique à Rust que nous voulons lier quelque chose à la variable maintenant. À droite du signe égal se trouve la valeur à laquelle guess est lié, qui est le résultat de l'appel à String::new, une fonction qui renvoie une nouvelle instance d'un String. String est un type de chaîne de caractères fourni par la bibliothèque standard qui est un texte encodé en UTF-8 et pouvant grandir.

La syntaxe :: dans la ligne ::new indique que new est une fonction associée du type String. Une fonction associée est une fonction qui est implémentée sur un type, dans ce cas String. Cette fonction new crée une nouvelle chaîne de caractères vide. Vous trouverez une fonction new sur de nombreux types car c'est un nom commun pour une fonction qui crée une nouvelle valeur d'un certain type.

En résumé, la ligne let mut guess = String::new(); a créé une variable mutable qui est actuellement liée à une nouvelle instance vide d'un String. Phew!

Receiving User Input

Rappelez-vous que nous avons inclus la fonctionnalité d'entrée/sortie de la bibliothèque standard avec use std::io; à la première ligne du programme. Maintenant, nous allons appeler la fonction stdin du module io, qui nous permettra de gérer l'entrée de l'utilisateur :

io::stdin()
 .read_line(&mut guess)

Si nous n'avions pas importé la bibliothèque io avec use std::io; au début du programme, nous pourrions toujours utiliser la fonction en écrivant cet appel de fonction comme std::io::stdin. La fonction stdin renvoie une instance de std::io::Stdin, qui est un type qui représente un pointeur vers l'entrée standard de votre terminal.

Ensuite, la ligne .read_line(&mut guess) appelle la méthode read_line sur le pointeur d'entrée standard pour obtenir l'entrée de l'utilisateur. Nous passons également &mut guess en tant qu'argument à read_line pour lui dire dans quelle chaîne de caractères stocker l'entrée de l'utilisateur. Le travail complet de read_line est de prendre tout ce que l'utilisateur tape dans l'entrée standard et d'ajouter cela à une chaîne de caractères (sans écraser son contenu), nous passons donc cette chaîne de caractères en tant qu'argument. L'argument chaîne de caractères doit être mutable afin que la méthode puisse modifier le contenu de la chaîne.

Le & indique que cet argument est une référence, qui vous donne un moyen de permettre à plusieurs parties de votre code d'accéder à une partie de données sans avoir besoin de copier ces données dans la mémoire plusieurs fois. Les références sont une fonctionnalité complexe, et l'un des principaux avantages de Rust est à quel point il est sécurisé et facile d'utiliser les références. Vous n'avez pas besoin de connaître beaucoup de ces détails pour terminer ce programme. Pour l'instant, tout ce que vous avez besoin de savoir est que, comme les variables, les références sont immuables par défaut. Par conséquent, vous devez écrire &mut guess plutôt que &guess pour la rendre mutable. (Le chapitre 4 expliquera plus en détail les références.)

Handling Potential Failure with Result

Nous travaillons toujours sur cette ligne de code. Nous discutons maintenant d'une troisième ligne de texte, mais notez qu'il s'agit toujours d'une seule ligne logique de code. La partie suivante est cette méthode :

.expect("Failed to read line");

Nous aurions pu écrire ce code comme ceci :

io::stdin().read_line(&mut guess).expect("Failed to read line");

Cependant, une longue ligne est difficile à lire, il est donc préférable de la diviser. Il est souvent sage d'introduire un retour à la ligne et d'autres espaces pour aider à découper les longues lignes lorsque vous appelez une méthode avec la syntaxe .method_name(). Maintenant, discutons de ce que fait cette ligne.

Comme mentionné précédemment, read_line met tout ce que l'utilisateur entre dans la chaîne que nous lui passons, mais elle renvoie également une valeur de type Result. Result est une énumération, souvent appelée enum, qui est un type qui peut être dans l'un de plusieurs états possibles. Nous appelons chaque état possible une variante.

Le chapitre 6 couvrira les enums en détail. Le but de ces types Result est de coder des informations de gestion d'erreurs.

Les variantes de Result sont Ok et Err. La variante Ok indique que l'opération a réussi, et à l'intérieur de Ok se trouve la valeur générée avec succès. La variante Err signifie que l'opération a échoué, et Err contient des informations sur la manière dont ou pourquoi l'opération a échoué.

Les valeurs du type Result, comme les valeurs de tout type, ont des méthodes définies sur elles. Une instance de Result a une méthode expect que vous pouvez appeler. Si cette instance de Result est une valeur Err, expect fera planter le programme et affichera le message que vous avez passé en argument à expect. Si la méthode read_line renvoie une Err, cela serait probablement le résultat d'une erreur provenant du système d'exploitation sous-jacent. Si cette instance de Result est une valeur Ok, expect prendra la valeur de retour que Ok contient et vous renverra juste cette valeur pour que vous puissiez l'utiliser. Dans ce cas, cette valeur est le nombre d'octets de l'entrée de l'utilisateur.

Si vous n'appelez pas expect, le programme compilera, mais vous obtiendrez un avertissement :

$ cargo build
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
warning: unused `Result` that must be used
  --> src/main.rs:10:5
   |
10 |     io::stdin().read_line(&mut guess);
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: `#[warn(unused_must_use)]` on by default
   = note: this `Result` may be an `Err` variant, which should be handled

warning: `guessing_game` (bin "guessing_game") generated 1 warning
    Finished dev [unoptimized + debuginfo] target(s) in 0.59s

Rust avertit que vous n'avez pas utilisé la valeur Result renvoyée par read_line, indiquant que le programme n'a pas géré une erreur possible.

La bonne manière de supprimer l'avertissement est d'écrire réellement du code de gestion d'erreurs, mais dans notre cas, nous voulons simplement faire planter ce programme lorsqu'un problème se produit, donc nous pouvons utiliser expect. Vous apprendrez à récupérer des erreurs au chapitre 9.

Printing Values with println! Placeholders

En dehors de la accolade fermante, il ne reste plus qu'une seule ligne à discuter dans le code jusqu'à présent :

println!("You guessed: {guess}");

Cette ligne imprime la chaîne de caractères qui contient maintenant l'entrée de l'utilisateur. Le groupe de parenthèses accoladées {} est un emplacement réservé : pensez à {} comme de petites pinces de crabe qui retiennent une valeur en place. Lorsque vous imprimez la valeur d'une variable, le nom de la variable peut être placé à l'intérieur des parenthèses accoladées. Lorsque vous imprimez le résultat de l'évaluation d'une expression, placez des parenthèses accoladées vides dans la chaîne de formatage, puis suivez la chaîne de formatage avec une liste séparée par des virgules d'expressions à imprimer dans chaque emplacement réservé de parenthèses accoladées vides dans le même ordre. Imprimer une variable et le résultat d'une expression dans un seul appel à println! serait comme ceci :

let x = 5;
let y = 10;

println!("x = {x} and y + 2 = {}", y + 2);

Ce code imprimerait x = 5 and y = 12.

Testing the First Part

Testons la première partie du jeu de devinette. Exécutez-le en utilisant cargo run :

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 6.44s
     Running `target/debug/guessing_game`
Guess the number!
Please input your guess.
6
You guessed: 6

À ce stade, la première partie du jeu est terminée : nous recevons l'entrée au clavier et nous l'imprimons ensuite.

Générer un nombre secret

Ensuite, nous devons générer un nombre secret que l'utilisateur tentera de deviner. Le nombre secret devrait être différent à chaque fois pour que le jeu soit amusant à rejouer. Nous utiliserons un nombre aléatoire entre 1 et 100 pour que le jeu ne soit pas trop difficile. Rust n'inclut pas encore de fonctionnalité de génération de nombres aléatoires dans sa bibliothèque standard. Cependant, l'équipe Rust fournit un paquet rand sur https://crates.io/crates/rand avec cette fonctionnalité.

Utiliser un paquet pour obtenir plus de fonctionnalités

Rappelez-vous qu'un paquet est une collection de fichiers de code source Rust. Le projet que nous avons construit est un paquet binaire, qui est un exécutable. Le paquet rand est un paquet de bibliothèque, qui contient du code destiné à être utilisé dans d'autres programmes et ne peut pas être exécuté seul.

La coordination de paquets externes par Cargo est là où Cargo brille vraiment. Avant d'être en mesure d'écrire du code utilisant rand, nous devons modifier le fichier Cargo.toml pour inclure le paquet rand comme dépendance. Ouvrez maintenant ce fichier et ajoutez la ligne suivante en bas, sous l'en-tête de section [dependencies] que Cargo a créé pour vous. Assurez-vous de spécifier rand exactement comme nous l'avons ici, avec ce numéro de version, sinon les exemples de code de ce tutoriel peuvent ne pas fonctionner :

Nom de fichier : Cargo.toml

[dependencies]
rand = "0.8.5"

Dans le fichier Cargo.toml, tout ce qui suit un en-tête est partie de cette section qui continue jusqu'à ce qu'une autre section commence. Dans [dependencies], vous dites à Cargo quels paquets externes votre projet dépend et quelles versions de ces paquets vous requérez. Dans ce cas, nous spécifions le paquet rand avec le spécificateur de version sémantique 0.8.5. Cargo comprend la Numérotation de version sémantique (parfois appelée SemVer), qui est une norme pour écrire des numéros de version. Le spécificateur 0.8.5 est en fait un raccourci pour ^0.8.5, ce qui signifie n'importe quelle version qui est au moins 0.8.5 mais inférieure à 0.9.0.

Cargo considère que ces versions ont des API publiques compatibles avec la version 0.8.5, et cette spécification vous assure que vous obtiendrez la dernière version de patch qui compilera toujours avec le code de ce chapitre. Aucune version 0.9.0 ou supérieure n'est garantie d'avoir la même API que celles utilisées dans les exemples suivants.

Maintenant, sans modifier aucun code, construisons le projet, comme indiqué dans la Liste 2-2.

$ cargo build
    Updating crates.io index
  Downloaded rand v0.8.5
  Downloaded libc v0.2.127
  Downloaded getrandom v0.2.7
  Downloaded cfg-if v1.0.0
  Downloaded ppv-lite86 v0.2.16
  Downloaded rand_chacha v0.3.1
  Downloaded rand_core v0.6.3
   Compiling rand_core v0.6.3
   Compiling libc v0.2.127
   Compiling getrandom v0.2.7
   Compiling cfg-if v1.0.0
   Compiling ppv-lite86 v0.2.16
   Compiling rand_chacha v0.3.1
   Compiling rand v0.8.5
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 2.53s

Liste 2-2 : La sortie de l'exécution de cargo build après avoir ajouté le paquet rand comme dépendance

Vous pouvez voir des numéros de version différents (mais tous seront compatibles avec le code, grâce à SemVer!) et des lignes différentes (en fonction du système d'exploitation), et les lignes peuvent être dans un ordre différent.

Lorsque nous incluons une dépendance externe, Cargo récupère les dernières versions de tout ce dont la dépendance a besoin depuis le registre, qui est une copie des données de Crates.io à https://crates.io. Crates.io est le lieu où les personnes du monde Rust publient leurs projets Rust open source pour que les autres puissent les utiliser.

Après avoir mis à jour le registre, Cargo vérifie la section [dependencies] et télécharge tous les paquets listés qui ne sont pas déjà téléchargés. Dans ce cas, bien que nous ayons seulement listé rand comme dépendance, Cargo a également récupéré d'autres paquets dont rand dépend pour fonctionner. Après avoir téléchargé les paquets, Rust les compile puis compile le projet avec les dépendances disponibles.

Si vous exécutez immédiatement cargo build à nouveau sans apporter de modifications, vous n'obtiendrez aucune sortie autre que la ligne Finished. Cargo sait qu'il a déjà téléchargé et compilé les dépendances, et vous n'avez rien changé à propos d'elles dans votre fichier Cargo.toml. Cargo sait également que vous n'avez rien changé à propos de votre code, donc il ne le recompile pas non plus. Ayant rien à faire, il quitte simplement.

Si vous ouvrez le fichier src/main.rs, apportez une modification triviale, puis enregistrez-le et reconstruisez-le, vous ne verrez que deux lignes de sortie :

$ cargo build
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 2.53 secs

Ces lignes montrent que Cargo ne met à jour la génération que avec votre petite modification au fichier src/main.rs. Vos dépendances n'ont pas changé, donc Cargo sait qu'il peut réutiliser ce qu'il a déjà téléchargé et compilé pour celles-ci.

Assurer des builds reproductibles avec le fichier Cargo.lock

Cargo a un mécanisme qui vous assure que vous pouvez reconstruire le même artefact chaque fois que vous ou quelqu'un d'autre compile votre code : Cargo utilisera uniquement les versions des dépendances que vous avez spécifiées jusqu'à ce que vous indiquiez le contraire. Par exemple, disons que la semaine prochaine est sortie la version 0.8.6 du paquet rand, et que cette version contient un correctif important de bogue, mais qu'elle contient également une régression qui casserait votre code. Pour gérer ceci, Rust crée le fichier Cargo.lock la première fois que vous exécutez cargo build, donc nous l'avons maintenant dans le répertoire guessing_game.

Lorsque vous construisez un projet pour la première fois, Cargo détermine toutes les versions des dépendances qui répondent aux critères puis les écrit dans le fichier Cargo.lock. Lorsque vous construisez votre projet plus tard, Cargo verra que le fichier Cargo.lock existe et utilisera les versions spécifiées là-dedans plutôt que de refaire tout le travail de détermination des versions. Cela vous permet d'avoir un build reproductible automatiquement. En d'autres termes, votre projet restera à 0.8.5 jusqu'à ce que vous le mettiez à niveau explicitement, grâce au fichier Cargo.lock. Étant donné que le fichier Cargo.lock est important pour les builds reproductibles, il est souvent inclus dans le contrôle de version avec le reste du code de votre projet.

Mettre à jour un paquet pour obtenir une nouvelle version

Lorsque vous voulez mettre à jour un paquet, Cargo fournit la commande update, qui ignorera le fichier Cargo.lock et déterminera toutes les dernières versions qui répondent à vos spécifications dans Cargo.toml. Cargo écrira ensuite ces versions dans le fichier Cargo.lock. Sinon, par défaut, Cargo ne cherchera que les versions supérieures à 0.8.5 et inférieures à 0.9.0. Si le paquet rand a publié les deux nouvelles versions 0.8.6 et 0.9.0, vous verriez ceci si vous exécutiez cargo update :

$ cargo update
Updating crates.io index
Updating rand v0.8.5 - > v0.8.6

Cargo ignore la version 0.9.0. À ce stade, vous remarqueriez également un changement dans votre fichier Cargo.lock indiquant que la version du paquet rand que vous utilisez maintenant est 0.8.6. Pour utiliser la version 0.9.0 de rand ou n'importe quelle version dans la série 0.9._x_, vous devriez mettre à jour le fichier Cargo.toml pour qu'il ressemble à ceci :

[dependencies]
rand = "0.9.0"

La prochaine fois que vous exécuterez cargo build, Cargo mettra à jour le registre des paquets disponibles et réévaluera vos exigences en rand selon la nouvelle version que vous avez spécifiée.

Il y a beaucoup plus à dire sur Cargo et son écosystème, que nous aborderons au Chapitre 14, mais pour l'instant, voilà tout ce que vous avez besoin de savoir. Cargo facilite grandement la réutilisation de bibliothèques, de sorte que les Rustaceans sont capables d'écrire de plus petits projets assemblés à partir de plusieurs packages.

Générer un nombre aléatoire

Commenceons à utiliser rand pour générer un nombre à deviner. La prochaine étape est de mettre à jour src/main.rs, comme indiqué dans la Liste 2-3.

Nom de fichier : src/main.rs

use std::io;
1 use rand::Rng;

fn main() {
    println!("Guess the number!");

  2 let secret_number = rand::thread_rng().gen_range(1..=100);

  3 println!("The secret number is: {secret_number}");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
     .read_line(&mut guess)
     .expect("Failed to read line");

    println!("You guessed: {guess}");
}

Liste 2-3 : Ajout de code pour générer un nombre aléatoire

Tout d'abord, nous ajoutons la ligne use rand::Rng; [1]. Le trait Rng définit les méthodes que les générateurs de nombres aléatoires implémentent, et ce trait doit être dans la portée pour que nous puissions utiliser ces méthodes. Le Chapitre 10 couvrira les traits en détail.

Ensuite, nous ajoutons deux lignes au milieu. Dans la première ligne [2], nous appelons la fonction rand::thread_rng qui nous donne le générateur de nombres aléatoires particulier que nous allons utiliser : celui qui est local au fil d'exécution actuel et est initialisé par le système d'exploitation. Ensuite, nous appelons la méthode gen_range sur le générateur de nombres aléatoires. Cette méthode est définie par le trait Rng que nous avons mis dans la portée avec l'instruction use rand::Rng;. La méthode gen_range prend une expression de plage en argument et génère un nombre aléatoire dans la plage. Le type d'expression de plage que nous utilisons ici prend la forme start..=end et est inclusive pour les bornes inférieure et supérieure, donc nous devons spécifier 1..=100 pour demander un nombre entre 1 et 100.

Note : Vous ne saurez pas tout de suite quels traits utiliser et quelles méthodes et fonctions appeler à partir d'un paquet, donc chaque paquet a une documentation avec des instructions pour l'utiliser. Une autre fonction pratique de Cargo est que l'exécution de la commande cargo doc --open construira la documentation fournie par toutes vos dépendances localement et l'ouvrira dans votre navigateur. Si vous êtes intéressé par d'autres fonctionnalités dans le paquet rand, par exemple, exécutez cargo doc --open et cliquez sur rand dans la barre latérale de gauche.

La deuxième nouvelle ligne [3] affiche le nombre secret. Cela est utile pendant que nous développons le programme pour pouvoir le tester, mais nous le supprimerons de la version finale. Ce n'est pas vraiment un jeu si le programme affiche la réponse dès le début!

Essayez d'exécuter le programme plusieurs fois :

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 2.53s
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 7
Please input your guess.
4
You guessed: 4

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.02s
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 83
Please input your guess.
5
You guessed: 5

Vous devriez obtenir des nombres aléatoires différents, et ils devraient tous être des nombres entre 1 et 100. Bravo!

#Comparer la proposition avec le nombre secret

Maintenant que nous avons une entrée utilisateur et un nombre aléatoire, nous pouvons les comparer. Cette étape est montrée dans la Liste 2-4. Notez que ce code ne compilera pas encore, comme nous allons l'expliquer.

Nom de fichier : src/main.rs

use rand::Rng;
1 use std::cmp::Ordering;
use std::io;

fn main() {
    --snip--

    println!("You guessed: {guess}");

  2 match guess.3 cmp(&secret_number) {
        Ordering::Less => println!("Too small!"),
        Ordering::Greater => println!("Too big!"),
        Ordering::Equal => println!("You win!"),
    }
}

Liste 2-4 : Gérer les valeurs de retour possibles de la comparaison de deux nombres

Tout d'abord, nous ajoutons une autre instruction use [1], qui importe un type appelé std::cmp::Ordering depuis la bibliothèque standard. Le type Ordering est un autre enum et a les variantes Less, Greater et Equal. Ces sont les trois résultats possibles lorsqu'on compare deux valeurs.

Ensuite, nous ajoutons cinq nouvelles lignes en bas qui utilisent le type Ordering. La méthode cmp [3] compare deux valeurs et peut être appelée sur tout ce qui peut être comparé. Elle prend une référence à ce que vous voulez comparer : ici, elle compare guess avec secret_number. Ensuite, elle renvoie une variante de l'enum Ordering que nous avons importé avec l'instruction use. Nous utilisons une expression match [2] pour décider de ce qu'il faut faire ensuite en fonction de la variante de Ordering qui a été renvoyée par l'appel à cmp avec les valeurs de guess et secret_number.

Une expression match est composée de bras. Un bras est composé d'un schéma à comparer, et du code qui devrait être exécuté si la valeur donnée à match correspond au schéma de cet bras. Rust prend la valeur donnée à match et parcourt successivement les schémas de chaque bras. Les schémas et la structure match sont des fonctionnalités puissantes de Rust : elles vous permettent d'exprimer diverses situations que votre code peut rencontrer et vous assurent de les gérer toutes. Ces fonctionnalités seront couvertes en détail respectivement au Chapitre 6 et au Chapitre 18.

Parcourons un exemple avec l'expression match que nous utilisons ici. Disons que l'utilisateur a deviné 50 et que le nombre secret généré aléatoirement cette fois est 38.

Lorsque le code compare 50 avec 38, la méthode cmp renverra Ordering::Greater car 50 est supérieur à 38. L'expression match reçoit la valeur Ordering::Greater et commence à vérifier chaque schéma d'arm. Elle regarde le schéma du premier bras, Ordering::Less, et constate que la valeur Ordering::Greater ne correspond pas à Ordering::Less, donc elle ignore le code de cet bras et passe au suivant. Le schéma du bras suivant est Ordering::Greater, qui correspond à Ordering::Greater! Le code associé à cet bras sera exécuté et affichera Too big! à l'écran. L'expression match se termine après le premier match réussi, donc elle ne regardera pas le dernier bras dans ce scénario.

Cependant, le code de la Liste 2-4 ne compilera pas encore. Essayons :

$ cargo build
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
error[E0308]: mismatched types
  --> src/main.rs:22:21
   |
22 |     match guess.cmp(&secret_number) {
   |                     ^^^^^^^^^^^^^^ expected struct `String`, found integer
   |
   = note: expected reference `&String`
              found reference `&{integer}`

Le cœur de l'erreur indique qu'il y a des types non compatibles. Rust a un système de types fort et statique. Cependant, il a également une inférence de type. Lorsque nous avons écrit let mut guess = String::new(), Rust a été capable d'inférer que guess devrait être une String et ne nous a pas obligé à écrire le type. En revanche, secret_number est un type numérique. Plusieurs types numériques de Rust peuvent avoir une valeur comprise entre 1 et 100 : i32, un nombre sur 32 bits ; u32, un nombre non signé sur 32 bits ; i64, un nombre sur 64 bits ; ainsi que d'autres. Sauf indication contraire, Rust utilise par défaut un i32, qui est le type de secret_number à moins que vous n'ajoutiez des informations de type ailleurs qui entraîneraient Rust à inférer un autre type numérique. La raison de l'erreur est que Rust ne peut pas comparer une chaîne de caractères et un type numérique.

En fin de compte, nous voulons convertir la String que le programme lit en entrée en un vrai type numérique pour pouvoir la comparer numériquement au nombre secret. Nous le faisons en ajoutant cette ligne au corps de la fonction main :

Nom de fichier : src/main.rs

use std::io;
use rand::Rng;
use std::cmp::Ordering;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    println!("The secret number is: {secret_number}");

    println!("Please input your guess.");

    let mut guess = String::new();

    io::stdin()
      .read_line(&mut guess)
      .expect("Failed to read line");

    let guess: u32 = guess
      .trim()
      .parse()
      .expect("Please type a number!");

    println!("You guessed: {guess}");

    match guess.cmp(&secret_number) {
        Ordering::Less => println!("Too small!"),
        Ordering::Greater => println!("Too big!"),
        Ordering::Equal => println!("You win!"),
    }
}

Nous créons une variable nommée guess. Mais attends, le programme n'a-t-il pas déjà une variable nommée guess? Oui, mais heureusement Rust nous permet de masquer la valeur précédente de guess avec une nouvelle. Le masquage nous permet de réutiliser le nom de variable guess plutôt que de devoir créer deux variables uniques, telles que guess_str et guess, par exemple. Nous aborderons ceci en détail au Chapitre 3, mais pour l'instant, sachez que cette fonction est souvent utilisée lorsque vous voulez convertir une valeur d'un type à un autre type.

Nous liisons cette nouvelle variable à l'expression guess.trim().parse(). Le guess dans l'expression fait référence à la variable guess d'origine qui contenait l'entrée sous forme de chaîne de caractères. La méthode trim sur une instance de String éliminera tout espace blanc au début et à la fin, ce que nous devons faire pour pouvoir comparer la chaîne avec le u32, qui ne peut contenir que des données numériques. L'utilisateur doit appuyer sur Entrée pour satisfaire read_line et entrer sa proposition, ce qui ajoute un caractère de nouvelle ligne à la chaîne. Par exemple, si l'utilisateur tape 5 et appuie sur Entrée, guess ressemble à ceci : 5\n. Le \n représente "retour à la ligne." (Sur Windows, appuyer sur Entrée résulte en un retour chariot et une nouvelle ligne, \r\n.) La méthode trim élimine \n ou \r\n, donnant juste 5.

La méthode parse sur les chaînes de caractères convertit une chaîne en un autre type. Ici, nous l'utilisons pour convertir d'une chaîne à un nombre. Nous devons dire à Rust le type numérique exact que nous voulons en utilisant let guess: u32. Le deux-points (:) après guess indique à Rust que nous allons annoter le type de la variable. Rust a plusieurs types numériques intégrés ; le u32 que l'on voit ici est un entier non signé sur 32 bits. C'est un bon choix par défaut pour un petit nombre positif. Vous apprendrez à propos d'autres types numériques au Chapitre 3.

De plus, l'annotation u32 dans ce programme d'exemple et la comparaison avec secret_number signifient que Rust devra également inférer que secret_number devrait être un u32. Maintenant, la comparaison sera entre deux valeurs du même type!

La méthode parse ne fonctionnera que sur des caractères qui peuvent logiquement être convertis en nombres et peut donc facilement entraîner des erreurs. Par exemple, si la chaîne contenait A👍%, il n'y aurait aucun moyen de la convertir en nombre. Comme cela peut échouer, la méthode parse renvoie un type Result, tout comme la méthode read_line (discutée précédemment dans "Gérer les échecs potentiels avec Result"). Nous traiterons ce Result de la même manière en utilisant à nouveau la méthode expect. Si parse renvoie une variante Err de Result parce qu'elle n'a pas pu créer un nombre à partir de la chaîne, l'appel à expect fera planter le jeu et affichera le message que nous lui donnons. Si parse peut convertir avec succès la chaîne en nombre, elle renverra la variante Ok de Result, et expect renverra le nombre que nous voulons à partir de la valeur Ok.

Exécutons le programme maintenant :

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 0.43s
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 58
Please input your guess.
  76
You guessed: 76
Too big!

Très bien! Même si des espaces ont été ajoutés avant la proposition, le programme a quand même compris que l'utilisateur avait deviné 76. Exécutez le programme plusieurs fois pour vérifier le comportement différent avec différents types d'entrée : devinez le nombre correctement, devinez un nombre trop élevé et devinez un nombre trop bas.

Nous avons maintenant la majeure partie du jeu fonctionnelle, mais l'utilisateur ne peut faire qu'une seule proposition. Modifions cela en ajoutant une boucle!

Autoriser plusieurs propositions avec une boucle

Le mot clé loop crée une boucle infinie. Nous allons ajouter une boucle pour donner aux utilisateurs plus de chances de deviner le nombre :

Nom de fichier : src/main.rs

use std::io;
use rand::Rng;
use std::cmp::Ordering;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    println!("The secret number is: {secret_number}");

    loop {
        println!("Please input your guess.");
        let mut guess = String::new();

        io::stdin()
         .read_line(&mut guess)
         .expect("Failed to read line");

        let guess: u32 = guess
         .trim()
         .parse()
         .expect("Please type a number!");

        println!("You guessed: {guess}");

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => println!("You win!"),
        }
    }
}

Comme vous pouvez le voir, nous avons déplacé tout à partir de l'invite d'entrée de la proposition jusqu'à la boucle. Assurez-vous d'indenter les lignes à l'intérieur de la boucle de quatre espaces supplémentaires chacune et exécutez le programme à nouveau. Le programme demandera désormais une nouvelle proposition à l'infini, ce qui introduit en fait un nouveau problème. Il semble que l'utilisateur ne puisse pas quitter!

L'utilisateur pourrait toujours interrompre le programme en utilisant le raccourci clavier ctrl-C. Mais il y a un autre moyen d'échapper à ce monstre insatiable, comme mentionné dans la discussion de parse dans "Comparer la proposition avec le nombre secret" : si l'utilisateur entre une réponse non numérique, le programme plantera. Nous pouvons en profiter pour permettre à l'utilisateur de quitter, comme montré ici :

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 1.50s
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 59
Please input your guess.
45
You guessed: 45
Too small!
Please input your guess.
60
You guessed: 60
Too big!
Please input your guess.
59
You guessed: 59
You win!
Please input your guess.
quit
thread'main' panicked at 'Please type a number!: ParseIntError
{ kind: InvalidDigit }', src/main.rs:28:47
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Taper quit quittera le jeu, mais comme vous le remarquerez, entrer n'importe quelle autre entrée non numérique le fera également. Cela est du moins suboptimal ; nous voulons que le jeu s'arrête également lorsque le nombre correct est deviné.

Quitter après avoir deviné correctement

Programmons le jeu pour qu'il quitte lorsque l'utilisateur gagne en ajoutant une instruction break :

Nom de fichier : src/main.rs

--snip--

match guess.cmp(&secret_number) {
    Ordering::Less => println!("Too small!"),
    Ordering::Greater => println!("Too big!"),
    Ordering::Equal => {
        println!("You win!");
        break;
    }
}

Ajouter la ligne break après You win! fait en sorte que le programme sorte de la boucle lorsque l'utilisateur devine correctement le nombre secret. Sortir de la boucle signifie également sortir du programme, car la boucle est la dernière partie de main.

Gérer les entrées invalides

Pour affiner encore le comportement du jeu, plutôt que de faire planter le programme lorsque l'utilisateur entre un non-nombre, faisons en sorte que le jeu ignore un non-nombre pour que l'utilisateur puisse continuer à deviner. Nous pouvons le faire en modifiant la ligne où guess est converti d'une String en un u32, comme montré dans la Liste 2-5.

Nom de fichier : src/main.rs

--snip--

io::stdin()
 .read_line(&mut guess)
 .expect("Failed to read line");

let guess: u32 = match guess.trim().parse() {
    Ok(num) => num,
    Err(_) => continue,
};

println!("You guessed: {guess}");

--snip--

Liste 2-5 : Ignorer une proposition non numérique et demander une autre proposition au lieu de faire planter le programme

Nous passons d'un appel à expect à une expression match pour passer de la plantation en cas d'erreur à la gestion de l'erreur. Rappelez-vous que parse renvoie un type Result et que Result est un enum qui a les variantes Ok et Err. Nous utilisons une expression match ici, comme nous l'avons fait avec le résultat Ordering de la méthode cmp.

Si parse est capable de convertir avec succès la chaîne en un nombre, elle renverra une valeur Ok qui contient le nombre résultant. Cette valeur Ok correspondra au schéma du premier bras, et l'expression match renverra simplement la valeur num que parse a produite et placée dans la valeur Ok. Ce nombre se retrouvera exactement où nous le voulons dans la nouvelle variable guess que nous créons.

Si parse n'est pas capable de convertir la chaîne en un nombre, elle renverra une valeur Err qui contient plus d'informations sur l'erreur. La valeur Err ne correspond pas au schéma Ok(num) dans le premier bras match, mais elle correspond au schéma Err(_) dans le second bras. Le tiret bas, _, est une valeur générique ; dans cet exemple, nous disons que nous voulons correspondre à toutes les valeurs Err, quelle que soit l'information qu'elles contiennent à l'intérieur. Le programme exécutera donc le code du second bras, continue, qui indique au programme d'aller à l'itération suivante de la loop et de demander une autre proposition. Ainsi, en fait, le programme ignore toutes les erreurs que parse pourrait rencontrer!

Maintenant, tout dans le programme devrait fonctionner comme prévu. Essayons :

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 4.45s
     Running `target/debug/guessing_game`
Guess the number!
The secret number is: 61
Please input your guess.
10
You guessed: 10
Too small!
Please input your guess.
99
You guessed: 99
Too big!
Please input your guess.
foo
Please input your guess.
61
You guessed: 61
You win!

Génial! Avec un dernier ajustement minuscule, nous terminerons le jeu de devinette. Rappelez-vous que le programme imprime toujours le nombre secret. Cela a bien fonctionné pour les tests, mais cela gâche le jeu. Supprimons l'instruction println! qui affiche le nombre secret. La Liste 2-6 montre le code final.

Nom de fichier : src/main.rs

use rand::Rng;
use std::cmp::Ordering;
use std::io;

fn main() {
    println!("Guess the number!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    loop {
        println!("Please input your guess.");

        let mut guess = String::new();

        io::stdin()
         .read_line(&mut guess)
         .expect("Failed to read line");

        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };

        println!("You guessed: {guess}");

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Too small!"),
            Ordering::Greater => println!("Too big!"),
            Ordering::Equal => {
                println!("You win!");
                break;
            }
        }
    }
}

Liste 2-6 : Code complet du jeu de devinette

À ce stade, vous avez construit avec succès le jeu de devinette. Félicitations!

Sommaire

Félicitations! Vous avez terminé le laboratoire de programmation d'un jeu de devinette. Vous pouvez pratiquer d'autres laboratoires dans LabEx pour améliorer vos compétences.