Bienvenue sur mon troisième blog sur Ratatui! Comme promis, dans le deuxième article, nous allons créer une application Ratatui pour une machine à sous. Mais nous n'allons pas passer directement au code. En tant que bons programmeurs que nous sommes, nous devons d'abord décomposer notre projet et planifier ce que nous prévoyons de faire dans ce projet.
Décomposition du Projet : Comprendre et Définir une Machine à Sous
Qu'est-ce qu'une Machine à Sous ?
Une machine à sous (1) est un jeu de hasard qu'on trouve dans les casinos. Le joueur fait une mise d'un certain montant d'argent. Lorsque les rouleaux s'arrêtent, une combinaison de symboles apparaît. Si cette combinaison correspond à l'une des combinaisons gagnantes prédéfinies, le joueur remporte un gain.
Éléments Clés d'une Machine à Sous
Dans notre projet, nous allons conserver les éléments essentiels d'une machine à sous. Voici ce que nous allons implémenter pour le restant des blogs:
-
3 Rouleaux :
Notre machine à sous aura 3 rouleaux, chacun contenant un symbole. -
Symboles et combinaisons gagnantes :
Les symboles sur les rouleaux déterminent si le joueur gagne ou perd. Par exemple :- 3 cerises alignées : gain de 10x la mise.
- 3 cloches alignées : gain de 20x.
- 3 lémons alignées : gain de 50x.
- Autres: Perd.
-
Mises :
Le joueur pourra choisir le montant de sa mise avant de tourner les rouleaux. Les gains seront calculés en fonction de la mise et de la combinaison gagnante obtenue. -
Solde du joueur : Le joueur dispose d'un montant initial d'argent qu'il peut utiliser pour placer ses mises.
-
Interface utilisateur (IU) :
Nous allons créer une interface utilisateur en terminal avec Ratatui pour afficher les rouleaux, les symboles, les mises, et les gains.
Conception de l'Interface Utilisateur
Maintenant que nous avons décomposé les éléments clés de notre machine à sous, passons à la conception de l'interface utilisateur. Notre objectif est de créer une interface intuitive et attrayante.
Éléments de l'interface utilisateur
Voici les principaux éléments que nous allons intégrer dans notre UI :
- Affichage des rouleaux
- Le montant de la mise
- Solde du Joueur
-
Interactions :
- Ajuster la mise.
- Lancer les rouleaux.
- Quitter le jeu.
Passons au code
Très bien! Pour le blog d'aujourd'hui, nous allons commencer par créer une interface utilisateur vide qui démarre. Le squelette principal sans les fonctionnalités principales que nous voulons, comme changer la mise et faire tourner les rouleaux, ou même des widgets ou de simples rectangles.
1. Configurer le projet
Comme si c'est un projet en rust, vous devez installer rustup. Une fois installé on crée le projet:
cargo new jackpottui
Pour exécuter des programmes Rust, faites :
cargo run
Une fois le projet crée, ouvrez Cargo.toml
, le manifest de rust.
[package]
name = "jackpottui"
version = "0.1.0"
edition = "2021"
[dependencies]
ratatui = "0.29.0" <---+ À ajouter
crossterm = "0.28.1" |
rand = "0.9.0" |
color-eyre = "0.6.3"---+
Les « crates » qu'on veut ajouter sont ratatui, crossterm, rand et color-eyre.
Pourquoi?
- ratatui: Pour la création d'interfaces utilisateur de terminaux en Rust.
- crossterm: Gestion des terminaux pour l'entrée et la sortie.
- rand: Génération de nombres aléatoires (Random number generator ou RNG) pour mélanger les symboles sur les rouleaux.
- color-eyre: Améliorer la gestion et l'affichage des erreurs avec formatage clair.
2. Structure de Base de l’Application
D'accord, le code ci-dessous peut sembler intimidant, mais c'est la structure principale de la plupart des applications Ratatui. Ce code configure notre application basée sur le terminal. Ça initialise le terminal.
Ce code est dans main.rs
:
use color_eyre::Result;
use crossterm::terminal::{EnterAlternateScreen, LeaveAlternateScreen};
use ratatui::crossterm::execute;
fn main() -> color_eyre::Result<()> {
color_eyre::install()?;
let mut stdout = std::io::stdout();
execute!(stdout, EnterAlternateScreen);
let mut terminal = ratatui::init();
let resultat = run(&mut terminal);
ratatui::restore();
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
resultat
}
Explication
Ceci configure notre interface à l'aide de ratatui et crossterm. Il initialise la gestion des erreurs avec color_eyre
(2) pour remplacer le gestionnaire de panique et d'erreurs par défaut, démarre un écran de notre terminal alternativement et initialise Ratatui. La fonction run()
(qu'on définira dessous) gère la logique principale de l'application. Enfin, il remet le terminal à son état original et revient à l'écran principal une fois la loop run()
termine.
3. La Boucle Principale
Toujours dans le fichier main.rs
, montrons maintenant la fonction run()
:
mod controle; <----------------------------+ À ajouter en haut avec le reste des use
mod iu; |
|
use ratatui::DefaultTerminal; |
use controle::traiter_événement_clavier; |
use iu::iu_machine::afficher_machine; -----+
fn run(terminal: &mut DefaultTerminal) -> Result<()> {
loop {
terminal.draw(|f| {
let size = f.size();
afficher_machine(f, size);
})?;
if let Err(_) = traiter_événement_clavier() {
break Ok(());
}
}
}
Explication
La fonction run()
gère la boucle principale. Elle affiche en continu notre interface utilisateur à l'aide de terminal.draw()
, en passant une fonction anonyme qui elle appelle afficher_machine()
, qui a f
, frame, et size
,qui est la résolution de notre terminal, en paramètre pour afficher l'interface de la machine à sous. On gère également les événements du clavier. Si l'utilisateur appuie sur « q », il quitte la boucle et ferme l'application.
Nous passerons premièrement sur comment traiter_événement_clavier()
gère le clavier et comment ça capture le boutton « q », puis on passe à affichier_machine()
.
Quant au mod en haut du code, nous y reviendrons plus en détail plus tard. Pour l'instant comprenez que les fonctionnalités de control.rs
et iu
sont maintenant rendues accessibles dans main.rs
.
4. Quitter le jeu
Dans le code dessus, si vous avez remarquer on import la fonction traiter_événement_clavier()
avec use controle::traiter_événement_clavier;
dans main.rs
. C’est parce que cette méthode se trouve dans le fichiercontrole.rs
. Alors, à l’intérieur du dossier src
allons créer le fichier controle.rs
. À l’intérieur de src/controle.rs
, le code est défini de cette manière:
use std::error::Error;
use crossterm::event;
use ratatui::crossterm::event::{Event, KeyEventKind};
pub type AppResultat<T> = std::result::Result<T, Box<dyn Error>>;
pub fn traiter_événement_clavier() -> AppResultat<()> {
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
match key.code {
event::KeyCode::Char('q') => return Err("erreur".to_string().into()),
_ => {}
}
}
}
Ok(())
}
Explication
Maintenant, ce code a l'air difficile! Vous ne trouvez pas? Eh bien, ne vous inquiétez pas. La méthode traiter_événement_clavier()
écoute les entrées du clavier à l'aide de event::read()
. Lorsqu'une touche est enfoncée (KeyEventKind::Press
) (3), elle vérifie quelle touche a été enfoncée. Si la touche « q » est enfoncée, le programme s'arrête et génère une erreur (Err("error".to_string().into())
) et pour les autres touches, il ne fait rien et continue (Ok(())
) (4).
Le type AppResultat<T>
, défini comme Result<T, Box<dyn Error>>
, simplifie la gestion des erreurs. Il représente soit une valeur de réussite (Ok(T)
) soit une erreur (Err(Box<dyn Error>)
), ce qui permet à la fonction de gérer tout type d'erreur de tous les Type
de Rust qui implémente le trait Error
, ce qui explique le dyn
(dynamique). En Rust, Result
est un Enum
, un type qui prend des variants. Result
prend les variants Ok(T)
etErr(E)
. Ainsi, le programme s'arrête lorsqu'une erreur est générée.
Box
(5) est un pointeur intelligent qui alloue son contenu sur le tas, ou heap en anglais. Il est nécessaire parce que la taille de dyn Error
n'est pas connue au moment de la compilation (6) parce que comme mentionner, on ne sait pas quelle Error
de quel type va être appelé. Un exemple concret de l'utilisation de Box<dyn Error>
serait par exemple deux types d'erreurs personnalisées. Nommons-les ErreurValidation et ErreurServeur. Mais comment faire si on veut une fonction qui retourne les deux types d'erreurs sans avoir deux fonctions douplons qui ont juste un retour différents? Dans ce cas on utilise Box<dyn Error>
.
5. Affichage de la machine à sous
Tous comme la fonction traiter_événement_clavier()
, la fonction afficher_machine()
est appelée avec use iu::iu_machine::afficher_machine;
dans main.rs
. C'est parce que cette méthode ce trouve dans le fichieriu
puis dans le module iu_machine
. Alors, à l'intérieur du dossier src
allons créer le dossier iu
puis à l'intérieur, le dossier iu_machine
. À l'intérieur de chacun des dossiers créer le fichier mod.rs
. L'objectif des fichiers mod.rs
en Rust est de définir les modules, organisant le code et gérant la visibilité des fonctions et la structure des projets. À l'intérieur de src/ui/mod.rs
ajouter: pub mod iu_machine;
pour indiquer que le module iu_machine
appartient au répértoire src/iu
. À l’intérieur de src/iu/iu_machine/mod.rs
, le code est défini de cette manière:
use ratatui::{layout::Rect, Frame};
pub fn afficher_machine(frame: &mut Frame, zone_principal: Rect) {}
Explication
Cette fonction va contenir tous les composants que nous souhaitons ajouter à notre machine à sous. Pour l'instant, elle est évidemment vide mais toujours visible lorsque nous démarrons notre application. Nous reviendrons plus en détail sur la manière dont sont définis les Widgets dans Ratatui la semaine prochaine. Tout sera consacré à la méthode afficher_machine()
.
Conclusion
Nous avons configuré Ratatui, créé la structure de base de notre machine à sous, et implémenté une interface vide. Prochaine étape : ajouter les widgets!
Code
Pour revenir au code vous pouvez accéder au Projet.
Références
- WIKIPEDIA, Text-based user interface, https://en.wikipedia.org/wiki/Slot_machine (Page consultée le 13 février 2025).
- RATATUI, Use
color_eyre
with Ratatui https://ratatui.rs/recipes/apps/color-eyre/ (Page consultée le 13 février 2025). - CROSSTERM, Enum KeyEventKind https://docs.rs/crossterm/latest/crossterm/event/enum.KeyEventKind.html#variant.Press (Page consultée le 13 février 2025).
- THE RUST REFERENCE, match expressions https://doc.rust-lang.org/reference/expressions/match-expr.html
- THE RUST PROGRAMMING LANGUAGE Using Box< T> to Points to Data on the Heap https://doc.rust-lang.org/book/ch15-01-box.html
- RUST BY EXAMPLE. Boxing errors https://doc.rust-lang.org/rust-by-example/error/multiple_error_types/boxing_errors.html (Page consultée le 14 février 2025).
Commentaires2
Utilité de stdout
Dans la fonction main du fichier main.rs il y a ce morceau de code :
Pourrais-tu m'expliquer son utilité? Le code au dessus importe la dépendance pour rendre les erreurs plus claires et le code en dessous crée l'interface de la machine à sous que tu est en train de coder dans la fonction run() mais ce morceau me semble inutile.
Re: Utilité de stdout
Salut Marielle!
Tu as raison de demander plus d'explications sur l’utilité de l'écran alternatif parce que je l'ai mentionné sans expliquer pourquoi on utilise
AlternateScreen
.AlternateScreen
est utile parce qu'il permet aux applications, comme dans notre cas pour la machine à sous, de prendre le contrôle complet de l'affichage du terminal sans changer l'état habituel du terminal de l'utilisateur. Cela évite de laisser le terminal en désordre. Une fois l'application fermée, le terminal revient exactement à son état d'origine.Par défaut, ton terminal s'exécute en écran principal. Lorsque tu exécutes une commande, la sortie s'affiche et une fois la commande terminée, elle reste visible.
Quand tu passes à l'écran alternatif, il cache le contenu de l'écran principal. Lorsque tu quittes l'écran alternatif, le terminal revient à son état initial, sans avoir perturber l'affichage de l'application et sans passer par la sortie standard.
Quand va venir le temps d'intéragir avec notre machine, on va changer notre mise et changer les trois rouleaux sans perturber l'affichage principal. En ce qui est la méthode
afficher_machine()
, tu peux l'a voir comme une page. Cette page va prendre toute l'espace de ton terminal. SansAlternateScreen
, ta commandecargo run
est toujours afficher en haut, mais caché parce que nous avons configuré la page pour qu'elle occupe tout l'espace. Tu peux pas l'a voir parce qu'on peut pas « scroller » parce que ça ne fait pas parti de nos contrôles.Pour une petite démo, il y un clip court dans la documentation officiel. Dans l'exemple, la « page » pour afficher
Hello World
ne prend pas tout l'écran car nous ne l'avons tout simplement pas définie de cette façon. L'exemple affiche directement unParagraph
, un Widget.Mais là, tu vas te demander que c'est une bonne pratique d'utiliser
AlternateScreen
. Alors pourquoi Ratatui ne définit pas par défautAlternateScreen
. Comme Rust, Ratatui donne le choix au programmeur. C'est une question d'explicité.