Bonjour! Voilà, nous arrivons à la fin de cette série d'articles. Le temps passe vite, et ce septième article marque la conclusion de cette série. Dans le blog précédent, ensemble, nous avons réussi à mettre en œuvre une fenêtre contextuelle pour nos utilisateurs afin d'ajouter plus d'interaction avec notre application machine à sous. Cette fenêtre permettra désormais de modifier la mise et d'ajouter de la monnaie. Aujourd'hui, nous nous assurerons de récupérer les chiffres réels représentant le montant de l'utilisateur et de récompenser ses gains. C'est parti!
La démonstration de ce que nous allons accomplir ensemble dans ce blog.
Quoi faire?
C'est effectivement assez ironique et amusant de savoir que nous travaillons sur une machine à sous et que nous n'avons pas encore les fonctionnalités principales d'une machine à sous après six articles complets. Pas d'inquiétude, c'est temps! Mais c'est précisément ce qui rend le développement d'un projet si intéressant. Chaque étape est importante et il faut parfois établir des bases solides et mettre en place des structures avant de se lancer dans les fonctionnalités principales.
Dans un tutoriel aussi simple que le nôtre, cela prouve que la construction d’une interface utilisateur, la gestion des contrôles des boutons du clavier et des fenêtres avec Ratatui demande beaucoup de préparation avant de pouvoir intégrer des fonctionnalités de base.
Comme vous avez vu dans la démo, on va pouvoir:
- Changer la mise
- Ajouter de la monnaie
- Afficher les paiements en fonctions de la mise actuelle
- Dépenser de l'argent
- Gagner de l'argent lorsque nous obtenons une combinaison de symboles
Commençons par l'affichage des prix et des combinaisons.
Affichage des paiements
Afficher les combinaisons
Pour garder les combinaisons possibles voulus, on va devoir créer un autre Struct appelé Montant
. Montant
va nous servir à ajuster la mise, le totale et les pertes. Pour les gains, c'est un cas spécial.
Créez un fichier nommé mod.rs
dans le nouveau dossier nommé montant dans src
(src/montant/mod.rs
).
Incluez le répértoire dans main.rs
en ajoutant mod montant;
Définir Montant
de cette manière:
pub struct Montant {
pub mise: f32,
pub total: f32,
pub combinaisons: Vec<(String, f32)>,
}
combinaison
est un vecteur qui permet uniquement à afficher les combinaisons de symboles cherchés. combinaison
garde des collections d'éléments de différents types, communément nommé des tuples. Il va garder les emojis représentant certains symboles avec leur prix.
Formater un vecteur
Si vous vous rappelez bien, on a une zone qui s'appelle
info_layout
, laquelle prend des textes avec le widget Ratatui Paragraph
. Le texte inséré dans Paragraph
ne prend pas en compte la taille des textes. Il ne saute pas de ligne automatiquement pour nous.
Comme la constante CONTENU
dans src/iu/constants.rs
affichait des informations erronées, nous devons la supprimer, car les constantes en Rust ne sont pas mutables. Nous devrons implémenter une solution alternative pour afficher les nouvelles informationss.
On va tout simplement créer une fonction appeler afficher qui retourne un vecteur contenant des string.
impl Montant {
pub fn afficher(&self) -> Vec<String> {
let mut affichage = Vec::new();
affichage.push(format!(
"Mise: ${:.2}\nTotale: ${:.2}",
self.mise, self.total
));
let combinaison = self
.combinaisons
.iter()
.map(|(symboles, retour)| format!("{} = ${:.2}", symboles, retour * self.mise))
.collect::<Vec<_>>()
.chunks(3)
.map(|groupe| groupe.join(" | "))
.collect::<Vec<_>>()
.join("\n");
affichage.push(combinaison);
affichage.push("TOURNER".to_string());
affichage
}
}
Explication
Wow ! Voici un exemple des avantages de Rust : grâce à ses itérateurs et fermetures, le langage permet d'écrire un code très concis et lisible. Les méthodes comme map
, collect
et chunks
facilitent le traitement des collections, rendant le code plus explicite.
Le premier élément du vecteur va nous permettre d'afficher la mise et le total acuelle. {:.2}
(1) est un spécificateur de format utilisé dans le formatage des macros comme format!
, dans notre cas. On itère combinaisons, formatant chaque paire (symboles, retour)
sous forme de chaîne affichant le symbole et une valeur calculée. Il regroupe ensuite ces chaînes formatées en blocs de trois, joignant chaque groupe par « | » et enfin concatène tous les groupes avec \n
pour sauter la ligne. Pourquoi multiplier retour * self.mise
? Lorsque la mise va être modifié, le prix changera également en fonction du pari. Puis, on ajoute "TOURNER" pour notre bouton.
Dans src/iu/iu_machine/mod.rs , changer par
for ((index, titre), contenue) in TITRE
.iter()
.enumerate()
.zip(application.montant.afficher().iter())
Dépense et gains
Cette partie est la plus facile, je vais vous montrer directement le code:
pub fn changer_mise(&mut self, mise: f32) {
self.mise = mise;
}
pub fn ajouter_total(&mut self, total: f32) {
self.total += total;
}
pub fn dépenser(&mut self) {
self.total -= self.mise;
self.perdu();
}
fn perdu(&mut self) {
if self.total <= 0.0 {
self.total = 0.0;
self.mise = 0.0;
}
}
Explication
Cet extrait de code définit des mutateurs permettant de modifier la mise, d'ajouter au total et de dépenser de l'argent. J'aimerais implémenter une gestion complète des pertes du joueur, mais par manque de temps, on s'assure simplement que lorsqu'il perd tout son argent, sa mise et son total restent à zéro. Il pourra se refinancer lui-même, mais il n'y a pas de gestion de fin de partie.
Soumettre l'entrée de l'utilisateur
Puisque c'est Application
qui gère la gestion des entrées utilisateur, on ajoute montant
comme nouveau champ.
pub struct Application {
pub mixeur: Mixeur,
pub symboles: Vec<Symbole>,
pub montant: Montant, <-----------------
pub saisie: Input,
pub saisie_mode: SaisieMode,
pub affichage_contextuel: bool,
pub type_contextuel: TypeContextuel,
}
Dans la méthodeinitiliser()
de l'Application, ajouter:
let montant = Montant {
mise: 2.0,
total: 450.0,
gains: vec![
("💠💠💠".to_string(), 2.0),
("🍺🍺🍺".to_string(), 2.2),
("⭐⭐⭐".to_string(), 2.0),
("🍒🍒🍒".to_string(), 1.0),
("🔔🔔🔔".to_string(), 2.0),
("🍋🍋🍋".to_string(), 1.4),
("🍌🍌🍌".to_string(), 2.0),
],
};
et passé montant
au constructeur Self
Pour soumettre une entrée, il faut d'abord définir dans quel type de fenêtre contextuelle nous sommes. Sommes-nous en train de changer la mise ou le total ? Nous allons gérer cette fonctionnalité de la manière suivante:
pub fn soumettre(&mut self) {
match self.type_contextuel {
TypeContextuel::Mise => self
.montant
.changer_mise(self.saisie.value().parse::<f32>().unwrap()),
TypeContextuel::Totale => self
.montant
.ajouter_total(self.saisie.value().parse::<f32>().unwrap()),
}
}
Et voilà, nous utilisons nos mutateurs pour modifier les champs du montant. C'est aussi simple que ça!
Étant donné que saisie.value()
retourne une chaîne de caractères (String
), il faut convertir son type, car les mutateurs attendent un f32
. En Rust, on utilise la méthode parse()
(2). La syntaxe peut paraître étrange, mais l'opérateur ::<T>
permet de spécifier en quel type nous voulons effectuer la conversion.
Controle
Il ne nous reste plus qu'à implémenter les fonctionnalités liées aux touches du clavier.
Pour la touche Entrée :
event::KeyCode::Enter => {
application.soumettre();
application.arrêter_édition();
}
Pour la touche Espace:
event::KeyCode::Char(' ') => {
application.mélanger_symboles();
application.montant.dépenser();
}
Calcul mathématique des prix
Et voilà ! La partie la plus intéressante et la plus importante de ce blog. Nous allons maintenant nous concentrer sur le calcul du gain lorsque le joueur obtient une combinaison. Gardez à l'esprit que nous avons 3 rouleaux et 7 symboles de poids différents. En fait, nous conserverons l'idée, comme pour une maison ou une entreprise, d'utiliser le taux de retour au joueur. Nous sommes une maison avare. Cela nous permet de conserver 55 % du gain du joueur. Ce qui veut dire que sur le long terme, pour chaque dollar misé, le joueur récupérera en moyenne 45 sous.
L'idée d'utiliser des multiplicateurs n'est pas du tout attrayante. Dire, par exemple, que la combinaison la plus rare rapporte 500 fois de la mise est injuste. Pourquoi? Dans notre contexte, les symboles sont pondérés. Certaines combinaisons seront plus rares que d'autres. Alors, comment déterminer le multiplicateur? Est-ce 500 fois ou 20 000 fois? Il est possible qu'à long terme, nous souhaitions ajuster les cotes. Les multiplicateurs changeront alors à nouveau.
Nous allons donc calculer les ratios en fonction de la probabilité. La formule est simple (3):
- Probabilité de la combinaison = P(A∩B∩C)=P(A)×P(B)×P(C)
- Paiment = Taux / Probabilité de la combinaison * Mise
Où A, B et C sont chacun calculé par = Cote/Poids totale Où poids totale est la somme de toutes les cotes de tous les symboles
Ex: 3 Diamants
- 1 diamant = cote = 1
- poids totale = 76
- Probabilité de la combinaison = (1/76)^3 = 1/4389761 ≈ 0.00000228 ≈ 0.000228%
- Paiment = 45% / 0.00000228 * 2 = 394,736 $
Conclusion: Après avoir avoir misé 2$, si j'obtient 3 diamants, je gagne 394,736 $.
Il existe des formules beaucoup plus sophistiquées. Cette formule nous va.
Codons
Tout les codes ajoutés sont dans Impl de Application application.rs
Commençons par le plus simple: Calculer le poids totale des symboles:
pub fn poids_total(symboles: &[Symbole]) -> f32 {
symboles
.iter()
.map(|symbole| symbole.pondération as f32)
.sum()
}
Parcourt un tableau de Symbole
, convertit la pondération de chaque symbole en f32
parce que pondération est en u8 , puis retourne la somme totale de ces pondérations sous forme de f32
.
Par la suite la probabilité de la combinaison, la fonction s'appelle pondérations()
:
pub fn pondérations(symboles: &[Symbole], combinaison: Vec<Type>) -> Vec<f32> {
combinaison
.iter()
.filter_map(|type_cherché| {
symboles
.iter()
.find(|&symbole| symbole.type_ == *type_cherché)
.map(|symbole| symbole.pondération.clone() as f32)
})
.collect::<Vec<_>>()
}
Prend en entrée un tableau de Symbole
et un vecteur de types, appelé combinaison
. Elle parcourt chaque élément de combinaison
puis par la suitere cherche un Symbole
correspondant au type dans le tableau symboles
à l'aide de find
. Si un Symbole
est trouvé, sa pondération est extraite, clonée, et convertie en f32
. Les pondérations obtenues sont par la suite collectées dans un vecteur.
Exemple de sortie: [14, 12, 1]
Passons maintenant à trouver le paiement idéal:
pub fn calculer_paiement_combinaison(symboles: &[Symbole], combinaison: Vec<Type>) -> f32 {
let mut probabilité = 1.0;
let taux_de_retour_au_joueur = 0.45;
for pondération in Self::pondérations(symboles, combinaison) {
probabilité *= pondération / Self::poids_total(symboles)
}
let paiement = taux_de_retour_au_joueur / probabilité;
paiement.round()
}
On itère la sortie de la fonction pondérations()
et calcul la probabilité:
Ex: 14/76 * 12/76 * 1/76
Et calcul le paiement et arrondi à l'unité près
Ce qui est plutôt intéressant avec la fonction calculer_paiement_combinaison()
c'est qu'elle est effectivement dépendante de l'état de Application, mais elle ne fait pas directement partie de la structure Application
. À la place, elle est définie dans l'implémentation de Application
(Impl), ce qui la rend disponible à travers la structure. Cependant, elle agit comme une fonction utilitaire pour calculer les paiements en fonction des symboles et de leurs pondérations, ce qui implique qu'elle doit avoir accès à certaines informations contenues dans Application
. Elle est plus utilitaire.
Changer montant dans Application:
À la place de passer des chiffres aux hazards comme deuxième éléments du tuple, utiliser par exemple:
Self::calculer_paiement_combinaison(&SYMBOLES, vec![Type::Diamant; 3])
Récompenser le joueur
pub fn payer(&mut self) {
if self.similaire(self.symboles.clone()) {
let symbole_type = self.symboles[0].type_.clone();
let montant_gagné =
Self::calculer_paiement_combinaison(&SYMBOLES, vec![symbole_type; 3])
* self.montant.mise;
self.montant.ajouter_total(montant_gagné);
}
}
pub fn similaire(&self, symboles: Vec<Symbole>) -> bool {
symboles
.get(0)
.map(|premier_symbole| symboles.iter().all(|x| x.type_ == premier_symbole.type_))
.unwrap_or(true)
}
Pour l'intstant, la seul manière de se faire récomponser, le joueur doit avoir 3 symboles similaires (4).
Ajouter par la suite payer()
à la touche touches du Espace:
event::KeyCode::Char(' ') => {
application.mélanger_symboles();
application.montant.dépenser();
application.payer(); <------------
}
Conclusion
Dans ce blog, nous avons implémenté la gestion des mises et d'argent en récupérant l'entrée utilisateur, l'affichage des gains et le calcul des probabilités de récompense.
La démonstration de ce que nous avons accompli ensemble dans ce blog.
Ce qui pourrait être intéressant d'ajouter:
- Enregistrez dans un fichier ou une base de données toutes les dépenses effectuées et les pertes.
- Une page d'accueil principale
- Gestion du jeu terminé
- Optez pour plus de 3 rouleaux (grille 4 x 5)
- Tour de bonus intense
- Jackpot progressif
- Graphique et animation
- Personnalisation de l'interface (de la machine)
- Multiplicateurs dynamiques
- Mini-jeux
- Suivi de statistique
Remerciement
Je tiens à remercier tous ceux qui m'ont soutenu et encouragé tout au long de la rédaction de ce tutoriel Ratatui. Merci également à la communauté de développeurs de Ratatui. Mais surtout, merci à vous, chers lecteurs, d'avoir pris le temps de suivre ce tutoriel. J'espère qu'il vous aura été utile et inspirant pour vos projets de développement.
Code
Pour revenir au code vous pouvez accéder au Projet.
Références
- STACK OVERFLOW, How does one round a floating point number to a specific number of digits, https://stackoverflow.com/questions/28655362/how-does-one-round-a-floating-point-number-to-a-specified-number-of-digits (Page consultée le 20 mars 2025).
- STACK OVERFLOW, Convert a String to Int https://stackoverflow.com/questions/27043268/convert-a-string-to-int (Page consultée le 20 mars 2025).
- Anne PERRUT, Cours de probabilités et statistiques , 2010, https://math.univ-lyon1.fr/irem/IMG/pdf/PolyTunis_A_Perrut.pdf (Page consultée le 21 mars 2025).
- GITHUB, _ Determining if a Rust Vector has all equal elements_ https://sts10.github.io/2019/06/06/is-all-equal-function.html (Page consultée le 21 mars 2025).
Commentaires1
Enrichissant
Super article! J'ai beaucoup aimé ta manière claire et structurée d'aborder les concepts techniques, ce qui facilite vraiment la compréhension, même pour quelqu'un qui n'est pas familier avec Rust. L'explication détaillée des calculs probabilistes était particulièrement intéressante! Selon toi, quelle fonctionnalité complémentaire (par exemple le jackpot progressif ou les mini-jeux) ajouterait le plus de valeur à une machine à sous comme celle-ci?