Bienvenue sur mon cinquième blog! Dans le blog précédent, nous avons créé une application Ratatui pour notre machine à sous qui affiche tous les composants de l'interface principale qui affiche de fausses données. Aujourd'hui, nous allons afficher de vrais symboles dans nos rouleaux en utilisant l'algorithme aléatoire pondéré et changer les combinaisons de nos trois symboles en appuyant sur la barre d'espace!
La démonstration de ce que nous allons accomplir ensemble dans ce blog
Comment aborder cela?
Par définition, une sélection aléatoire pondérée (1) est une méthode dans laquelle:
- Chaque résultat possible se voit attribuer un poids de probabilité.
- Chaque résultat n'ont pas de chance égale d'être sélectionné.
Cela signifie que certains résultats sont plus susceptibles de se produire que d'autres, en fonction des poids qui leur sont attribués.
La formule: (F(i)=(Pi / n∑j=1 Pj) Où nous obtenons le poids d'un événement dont nous voulons obtenir la probabilité divisé par la somme des pondérations de la séquence d'éléments commençant par le premier élément, j=1, jusqu'à n, le dernier.
En termes de programmation, nous ajouterons l'attribut « pondération » à notre struct Symbole. Pour le calcul de pondération, c'est super simple à mettre en œuvre, mais pourquoi l'écrire nous-mêmes alors qu'on a déjà un « crate » pour le faire à notre place: rand. rand, « crate » le plus utilisé, est un ensemble de « crate » supportant des générateurs de nombres aléatoires.
Nous avions déjà ajouté rand dans notre fichier Cargo.toml
. Eh bien, il est enfin temps de l'utiliser.
Ajout d'art ASCII pour le reste des symboles
Avant de passé à la nouvelle implémentation, vous trouverez ici les dessins ASCII pour les symboles restants que nous avons ignorés dans le dernier blog: Cloche, Cerise, Bière, Étoile et Banane. Également vous trouverez ici le retour des Tuple
pour ces symboles aussi.
Implémentation des symboles aléatoires
1. Ajout de l'attribut:
Comme mentionné, ajoutons l'attribut pondération
dans notre Struct Symbole qui est de type u8, le plus petit entier non signé, positif, en Rust:
pub struct Symbole {
pub type_: Type,
pub pondération: u8,
}
2. Responsable des pondérations:
Pour gérer la pondération des symboles, nous allons créer un nouveau Sruct appelé Mixeur. Créons alors le fichier mixeur.rs
(src/symboles/mixeur.rs
). Puis, comme d'habitude quand vous ajouter un nouveu fichier, ajouter pub mod mixeur;
en haut du mod.rs
qui partage le même répértoire que mixeur.rs
(src/symboles/mod.rs
).
On va définir Mixeur
de cette manière. Elle va a contenir l'attributs rouleaux qui va contenir tous les symboles possibles:
pub struct Mixeur {
pub rouleaux: Vec<Symbole>,
}
Nous allons créer notre première fonction associée qui s'appelle symboles()
. Dans le bloc impl Mixeur {}
, qui est à ajouter, symboles()
ressemblent à ceci:
pub fn symboles() -> Self {
Self {
rouleaux: vec![
Symbole {
type_: Type::Citron,
pondération: 20,
},
Symbole {
type_: Type::Cloche,
pondération: 19,
},
Symbole {
type_: Type::Cerise,
pondération: 15,
},
Symbole {
type_: Type::Bière,
pondération: 14,
},
Symbole {
type_: Type::Étoile,
pondération: 14,
},
Symbole {
type_: Type::Banane,
pondération: 21,
},
Symbole {
type_: Type::Diamant,
pondération: 7,
},
],
}
}
Explication
En Rust, quand vous souhaitez appeler des méthodes à partir d'une instance, vous ajoutez &self
dans la signature de la méthode, ce qui signifie simplement « emprunter l'instance actuelle ». Vous appelez une méthode, qui prend &self
comme paramètre, avec un point. Mais, pour les cas spéciaux, où vous souhaitez utiliser une fonction associée (2), ou associated function en anglais, vous ne mettez pas l'instance actuelle dans la signature. Pour utiliser une fonction associée, vous n'avez pas besoin de créer une instance puis de l'appeler avec un point. À la place, vous l'utilisez PENDANT la création de l'instance avec le double deux points, ::
.
Le bloc Self { }
dans la fonction est le corps du constructeur qui revoie une instance de Self
. En Rust, Self
, avec « s » majuscule, veut dire le type de l'instance. Dans le retour (-> Self
) et le constructeur, on a pu spécifié Mixeur
, mais ça ne sert à rien parce que la fonction associée est dans le bloc Impl
. Elle s'associe au type. On va utiliser symboles()
pour peupler l'instance Mixeur
pendant sa création.
3. Calcul des pondération:
En ce qui est pour le calcul de la pondération, nous utiliserons WeightedIndex. Créons une nouvelle méthode dans le bloc impl Symbole
appelé mélanger:
pub fn mélanger(&self, liste: &Vec<Symbole>) -> Vec<Symbole> {
let symboles_pondérés =
WeightedIndex::new(self.rouleaux.iter().map(|symbole| symbole.pondération))
.expect("Invalide");
let mut symboles = Vec::new();
for _ in 0..3 {
symboles.push(liste[symboles_pondérés.sample(&mut rand::rng())].clone());
}
symboles
}
importez en haut du fichier:
use rand::distr::{weighted::WeightedIndex, Distribution};
Explication
Quant à la méthode mélanger()
, nous lui passons un vecteur qui contient tous les symboles possibles. Nous nous attendons à ce qu'il renvoie un vecteur contenant 3 symboles, vu qu'on a 3 rouleaux.
On utilise WeightedIndex::new()
pour créer une ditribution pondérée. On itère le vecteur que l'attribut rouleaux
de Mixeur
contient pour extraire la pondération des symbole en utilisant la fonction anynome , ou pour être plus propre, un « closure » , et on injecte la pondération dans symboles_pondérés
avec map()
. Pour des cas excéptionnel, on ajoute expect()
pour gérer les erreurs. WeightedIndex::new()
retourne Result<WeightedIndex<X>, Error>
. Comme on a vu dans le troisième blog, Result
est un Enum
, un type qui prend des variants. Result
prend les variants Ok(T)
etErr(E)
.
On crée un nouveau vecteur symboles
qui va comprendre nos 3 symboles. On itère 3 fois pour sélectionner 3 symboles aléatoirement en fonction de leur probabilité avec sample()
qui retourne l'index à partir du WeightedIndex
. Avec l'index, on ajoute à symboles
le symbole représenté par l'index du vecteur liste
qu'on a passé en paramètre.
Pour se basé, d'un exemple plus simple vous pouvez consulter la documentation officiel.
4. Testons
Non, nous n'allons pas encore l'essayer dans l'interface. Nous devons encore faire quelques tests unitaires. Vous vous demandez probablement maintenant comment nous allons pouvoir tester les combinaisons de symboles lorsque l'événement n'est pas prévenu.
On peut tous simplement tricher le système, notre propre code. On pourrait exagérer la pondération de nos éléments (10 contre 50 par exemple).
Créons un nouveau dossier nommé tests
dans src/tests
. Par la suite, créons mod.rs
dans src/tests/mod.rs
. Intégrons le dossier tests
dans main.rs
avec mod tests;
On pourrait utiliser le fichiermod.rs
pour écrire nos tests. Mais il est préférable de créer un autre fichier nommé mixeur.rs
dans src/tests/mixeur.rs
. Pourquoi? La séparation des tests permet d'organiser le projet. Les fonctions utilitaires peuvent être partagées entre plusieurs tests. Qu'est ce que je veut dire par là? Disons que nous voulons compter le nombre de pièces en échec par type qui sont blanches ou noires qui restent après une capture de pièce ou après qu'une pièce a bougé. Nous allons utiliser la nouvelle fonction dans roi.rs
, pion.rs
et autres pièces comme je l'ai fait dans mon projet d'échecs. Ça réduit la duplication du code et le rend plus facile à maintenir. Dans ce cas, on va utiliser mod.rs
Pour organiser un fichier de test nous devons ajouter l'annotation #[cfg(test)]
(3) pour dire au compilateur qu'on va exécuter uniquement les tests quand on fait cargo test
. cfg
veut dire configuration. mod tests {}
, de sa part, définit un module pour les tests unitaire. Compilés que pendant les tests.
Pour visualiser les tests, cliquez ici
5. Essayons la fonctionnalité:
Avant de tester la fonctionnalité, effacer ce code dans src/iu/iu_machine/mod.rs
qui nous permettait de simuler les symboles:
let citron = Symbole {
type_: Type::Citron,
};
let diamant = Symbole {
type_: Type::Diamant,
};
let symboles = [&citron, &diamant, &citron];
À la place, écrivez:
let mixeur = Mixeur::symboles();
let symboles = mixeur.mélanger(&mixeur.rouleaux);
et n'oubliez pas d'importer Mixeur.
Démarrez le programme avec cargo run
et voici nos symboles. Quittez avec « q » et exécutez à nouveau le programme pour voir des symboles différents cette fois.
Implémenter la fonctionnalité tourner
Maintenant, nous allons voir ensemble comment nous pouvons recevoir différentes combinaisons des 3 symboles tout en restant dans l'instance de notre application.
1. Définir le Struct Application:
Afin de gérer la liste des symboles aléatoires, nous devons trouver un moyen d'effacer et de remplir la liste à chaque fois que nous appuyons sur la barre d'espace. Devrions-nous en faire une variable globale puisque c'est notre principale source de notre application? ABSOLUMENT PAS, personnellement. Dans ce cas, nous devons créer un composant capable de gérer le composant principal de notre programme, qui est la liste, et de gérer l'état de notre machine à sous. Il doit conserver la logique de génération de symboles. Je pense personnellement à créer un Struct. J'ai en tête le nom Noyau, Engin ou Application. Utilisons Application.
Pour l'instant, nous avons qu'à gérer nos symboles. En gardant la génération des symboles, nous avons besoin du Mixeur. Au lieu de supprimer des éléments de notre vecteur, nous allons conserver les symboles comme attribut et lorsque nous appuyons sur la barre d'espace, nous écrasons les symboles avec les nouveaux symboles qui seront conservés dans l'attribut symboles.
Alors, créons le fichier application.rs
dans src/application.rs
et définissons notre Struct Application
de cette manière:
pub struct Application {
pub mixeur: Mixeur,
pub symboles: Vec<Symbole>,
}
2. Constructeur
Comme avec le Mixeur
, on va créer une fonction associée afin de la populer pendant l'instanciation. L'appelons initialiser()
. À chaque foit qu'on ajoute une fonction à un Struct
ou un Enum
ou autre, on l'a met dans Impl {Nom}
:
pub fn initialiser() -> Self {
let mixeur = Mixeur::symboles();
let symboles = mixeur.mélanger(&mixeur.rouleaux);
Self { mixeur, symboles }
}
Parce que les attributs et les variables qu'ils contiendront partagent le même nom, nous n'avons pas besoin de spécifier l'attribut dans notre constructeur. Ça s'appelle « Field init shorthand » (4).
3. Mutateur
Comme mentionné, on va devoir écraser les symboles par les nouveaux symboles. C'est simple, pour le faire nous allons prendre l'attribut symboles
de l'instance actuelle de Application
et utiliser la méthode mélanger
pour recevoir de nouveau les 3 nouveaux symboles. Appelons la méthode mélanger_symboles()
:
pub fn mélanger_symboles(&mut self) {
self.symboles = self.mixeur.mélanger(&self.mixeur.rouleaux);
}
On n'a pas encore vu &mut self
dans nos blogs. Ça (5) représente une référence mutable à notre instance. Elle permet de modifier l'instance.
4. Contrôle
Votre expression match
dans le fichier controle.rs
(src/controle.rs
) va maintenant ressemblé à ça:
match key.code {
event::KeyCode::Char('q') => return Err("erreur".to_string().into()),
event::KeyCode::Char(' ') => application.mélanger_symboles(),
_ => {}
}
ajouter égalementapplication: &mut Application
dans la signature de la fonction traiter_événement_clavier()
.
On est obligé de définir application mutable parce qu'on change sont état dans sa méthode mélanger_symboles()
.
Tout comme « q », à la place on va prendre une chaîne vide qui représente le saut de la colonne par la barre d'espace. Quand on l'appui, la méthode mélanger_symboles()
est appelé.
5. Instancier Application
Voilà, on arrive vers la fin. Tout ce qu'il nous reste à faire et d'initialiser Application
. Pour le faire, on va dans main.rs
. À l'intérieur de run()
et l'extérieur de la block loop
, ajoutez:
let mut application = Application::initialiser();
Passer également à traiter_événement_clavier()
notre application:
if let Err(_) = traiter_événement_clavier(&mut application) {
break Ok(());
}
Vu que notre IU de notre machine demande, la liste des symboles, on va également passer à afficher_machine()
application:
Votre méthode run ressemble désormais à ceci:
fn run(terminal: &mut DefaultTerminal) -> Result<()> {
let mut application = Application::initialiser();
loop {
terminal.draw(|f| {
let size = f.area();
afficher_machine(f, size, &mut application);
})?;
if let Err(_) = traiter_événement_clavier(&mut application) {
break Ok(());
}
}
}
6. Passer la liste des symboles à l'IU
Maintenant que vous êtes dans src/iu/iu_machine/mod.rs
, ajouter dans la signature de la méthode afficher_machine()
:
application: &mut Application
Puis, effacer le code qu'on avez ajouté dans ce blog:
let mixeur = Mixeur::symboles();
let symboles = mixeur.mélanger(&mixeur.rouleaux);
et à la place de passé symboles
à l'iterateur, on passe application.symboles
.
Comme ça:
for (index, symbole) in application.symboles.iter().enumerate() {
frame.render_widget(
Paragraph::new(symbole.dessin().to_string())
.alignment(Alignment::Center)
.block(
Block::new()
.padding(Padding::uniform(8))
.borders(Borders::ALL)
.border_type(BorderType::QuadrantInside)
.style(Style::default().fg(symbole.couleur())),
),
layout_rouleaux[index],
);
}
Conclusion
Dans ce blog, nous avons implémenté la sélection de symboles aléatoires pondérés pour notre machine à sous et intégré une fonctionnalité pour actualiser les rouleaux à l'aide de la barre d'espace.
La démonstration de ce que nous avons accompli ensemble dans ce blog
La semain prochaine
La semaine prochaine, nous nous concentrerons sur la gestion des mises et du total des joueurs dans notre machine à sous. Implémenter la modification de mises sera abordée plus tard.
Code
Pour revenir au code vous pouvez accéder au Projet.
Références
- WIKIPEDIA, Weighted arithmetic mean, https://en.wikipedia.org/wiki/Weighted_arithmetic_mean (Page consultée le 27 février 2025).
- THE RUST REFERENCE, Associated Items https://doc.rust-lang.org/reference/items/associated-items.html#associated-functions-and-methods (Page consultée le 27 février 2025).
- THE RUST PROGRAMMING LANGUAGE, Test Organization https://doc.rust-lang.org/book/ch11-03-test-organization.html#the-tests-module-and-cfgtest (Page consultée le 27 février 2025).
- THE EDITION GUIDE, Field init shorthand https://doc.bccnsoft.com/docs/rust-1.36.0-docs-html/edition-guide/rust-2018/data-types/field-init-shorthand.html (Page consultée le 28 février 2025).
- STACK OVERFLOW, When to use self, &self, &mut self in methods https://stackoverflow.com/questions/59018413/when-to-use-self-self-mut-self-in-methods (Page consultée le 28 février 2025).
Commentaires1
Salut, AlikhanWow, merci…
Salut, Alikhan Wow, merci pour ton guide simple et efficace pour créer des symboles aléatoires sur la machine à sous, étape par étape. C'était tellement clair, surtout que j'ai vu ta démonstration en vidéo. Bonne continuation !