Conserver une feuille de score valide entre les manches

RustBeginner
Pratiquer maintenant

Introduction

Un score ne représente qu'une décision. Une partie complète doit mémoriser les catégories utilisées et conserver les mêmes dés pendant la correction d'un choix invalide. Vous allez terminer un jeu à trois manches et enregistrer une feuille vérifiable en recalculant les points à partir des dés et des catégories enregistrés.

Les règles de calcul du premier laboratoire sont déjà installées dans ce projet indépendant. Vous devez connaître ces règles, les fonctions Rust, enum, match, Option, les tableaux et les chemins de base dans un terminal. Ce laboratoire présente les structures préparées, les méthodes mutables, Result et les slices au moment de les utiliser. Prévoyez environ 55 minutes, lecture et erreurs courantes comprises ; il s'agit d'une estimation de l'auteur.

Valider uniquement un choix légal

Dans cette étape, concevez et implémentez la transition d’état d’un choix accepté. Commencez dans l’onglet Terminal. Ce projet indépendant possède déjà les règles de score complètes. Il n’utilise pas les fichiers du premier laboratoire. Réglez le chemin de recherche de ce shell et entrez dans le projet ; répétez ces commandes dans un nouveau shell :

export PATH=/home/labex/.cargo/bin:$PATH
cd /home/labex/project/dice-game

Commencez par un score normal fonctionnel et prévoyez-le avant de lancer :

cargo run --quiet -- score pair 2 2 4 4 6

Attendez les dés affichés et pair: 8. Cela prouve que la couche de règles fournie est utilisable avant d’ajouter l’état de la partie. Lisez le squelette :

cat src/state.rs

Un struct regroupe des champs nommés décrivant un objet. Round enregistre cinq dés, une catégorie et les points. Game contient current: Option<Dice> et rounds: Vec<Round>. Some(dice) signifie qu’un choix est en attente ; None qu’il n’y a pas de lancer courant. Vec<Round> est une collection extensible ; Vec::new() en crée une vide et push ajoute un élément.

Lisez aussi l’appelant dans l’interface fournie :

cat src/main.rs

Repérez match game.submit(input.trim()). Sa branche Err affiche un refus. Sa branche Ok affiche les points, puis obtient un nouveau lancer uniquement si moins de trois manches sont enregistrées. Vous ne modifiez pas cette interface. Votre méthode doit laisser l’état inchangé sur refus pour que l’invite suivante affiche les mêmes dés et le même compteur de lancers.

Prévoyez les champs après chaque type de soumission :

Soumission rounds après l’appel current après l’appel
Aucun dé en attente, mot inconnu ou catégorie utilisée Inchangé Inchangé
Catégorie valide inutilisée, y compris un score nul Un nouvel enregistrement complet ajouté None

C’est l’interface, pas submit, qui fournit le lancer suivant après un succès. Ne tirez pas de dés dans votre méthode. impl Game regroupe les méthodes de Game. &mut self permet à cette méthode de modifier la partie par référence mutable. Result<u16, &'static str> signifie Ok(points) ou Err(message) ; &'static str est le type du texte fixe des messages d’erreur. Contrairement à Option, Result peut expliquer un échec.

ok_or(message) transforme Some en Ok et None en Err. Le ? suivant continue avec la valeur réussie ou renvoie immédiatement l’erreur. Par exemple, le contrôle d’un lancer en attente peut être :

let dice = self.current.ok_or("match finished")?;

Appliquez la même idée à Category::parse(input). Avant de modifier un champ, empruntez les enregistrements précédents avec une boucle for sur &self.rounds et refusez une catégorie identique. Vous choisissez l’organisation des contrôles ; le tableau est le contrat. Après validation, obtenez points avec score(dice, category). La validation réussie comporte trois actions :

self.rounds.push(Round { dice, category, points });
self.current = None;
Ok(points)

Les noms de champs abrégés utilisent les variables locales de même nom. Un résultat nul est toujours un succès. Aucune de ces modifications ne doit appartenir à un chemin refusé.

Gardez inchangés src/main.rs, Cargo.toml, les types publics, les noms et types de fonctions et les exportations de modules. use crate::{score, Category, Dice} importe depuis la bibliothèque courante ; mod state et pub use dans src/lib.rs relient les modules. Cette étape modifie uniquement submit dans src/state.rs, en laissant ses structs, sa méthode new et son replay inachevé intacts.

Avant de modifier, faites une sauvegarde unique de ce fichier autorisé. Comme enseigné, mktemp -d remplace six X par un suffixe unique. $() stocke le chemin affiché ; && n’exécute cp -p qu’après la réussite de création du répertoire. Le chemin entre guillemets reste un seul argument. En cas d’échec, lisez l’erreur et obtenez une sauvegarde avant de modifier.

rs02_before_submit=$(mktemp -d .edit-before-submit.XXXXXX) && cp -p src/state.rs "$rs02_before_submit/state.rs"

Ouvrez le fichier avec nano ; -c affiche la position du curseur :

nano -c src/state.rs

Appuyez sur Ctrl+W, recherchez pub fn submit, puis appuyez sur Enter. Inspectez les accolades ouvrante et fermante de cette méthode. Renommez le paramètre inutilisé _input en input sans changer son type. Remplacez uniquement le corps inachevé par votre implémentation des contrôles et de la validation réussie ci-dessus. Préservez les autres méthodes et définitions de données. Utilisez les flèches et Backspace/Delete pour modifier. Enregistrez avec Ctrl+O, Enter, puis quittez avec Ctrl+X. La notation ^ de nano signifie Ctrl. Si une modification accidentelle n’est pas enregistrée, Ctrl+X, puis N à la question l’abandonne ; cela n’annule pas un changement déjà enregistré.

Relisez la méthode enregistrée avant de lancer :

cat src/state.rs

Utilisez la séquence fixe connue pour observer vos décisions d’état. --fixture sélectionne les lancers annoncés plutôt que la source aléatoire :

cargo run --quiet -- play --fixture

Attendez chaque invite, puis saisissez un mot et appuyez sur Enter :

Saisissez d’abord pairs. Ce mot est inconnu : attendez les mêmes [2, 2, 4, 4, 6] et rolls 1 après le refus. Saisissez ensuite pair. Il rapporte 8 et passe à [1, 2, 3, 4, 6], rolls 2. Saisissez à nouveau pair. La catégorie déjà utilisée est refusée, laissant ces dés et rolls 2 inchangés. Saisissez alors straight. Ce choix valide rapporte zéro et passe au troisième lancer. Saisissez enfin sum. Il rapporte 20.

L’erreur finale attendue est Error: replay not implemented, avec le statut 1. Vos soumissions ont fonctionné, mais replay reste la fonction inachevée originale : l’interface refuse donc d’enregistrer une feuille non vérifiée. C’est l’état intermédiaire prévu, pas une partie enregistrée réussie. Si vous choisissez accidentellement une autre catégorie légale, terminez les choix inutilisés ou utilisez Ctrl+C et démarrez volontairement une nouvelle partie fixe. Dans une partie, un refus ne doit rien redémarrer ni relancer.

Pour une erreur de compilation, lisez le fichier et la ligne signalés, rouvrez nano, recherchez la méthode concernée et corrigez-la. Pour une erreur enregistrée que vous ne pouvez pas corriger, restaurez votre propre fichier d’avant submit, puis implémentez et testez submit à nouveau :

Uniquement pour récupérer une erreur enregistrée : dans le Terminal d’origine, la variable du point de sauvegarde contient encore son chemin. Dans un nouveau Terminal, répétez d’abord les commandes PATH et de répertoire du projet. Énumérez les sauvegardes de cette phase :

ls -d .edit-before-submit.*

ls -d liste les noms de répertoires plutôt que leur contenu ; * correspond aux suffixes des répertoires uniques de cette phase. Ce sont de vrais noms générés : ne saisissez pas de suffixe inventé. Sans correspondance, confirmez le répertoire du projet et la création de ce point de sauvegarde ; ne copiez pas depuis une autre phase. Pour sélectionner l’un des chemins affichés, lancez :

read rs02_before_submit

Le shell attend une ligne. Collez le nom exact du répertoire choisi et appuyez sur Enter. read stocke cette ligne dans la variable nommée pour cette session. Confirmez le contenu de la sauvegarde avant de restaurer :

cat "$rs02_before_submit/state.rs"

Elle doit contenir le code initial avant submit. Si le chemin ou la phase est incorrect, répétez read avec le bon chemin affiché et inspectez à nouveau. Utilisez seulement ensuite la commande de restauration. Ce contrôle vaut aussi dans le Terminal d’origine si vous ne savez plus quel point de sauvegarde sa variable désigne.

cp -p "$rs02_before_submit/state.rs" src/state.rs

Cette commande remplace volontairement uniquement le fichier d’état modifiable. La variable de sauvegarde est locale à ce Terminal ; utilisez les instructions de récupération conditionnelle dans un autre shell. Ne relancez pas setup et ne remplacez pas l’interface préparée pour corriger une erreur d’état.

Recalculer et enregistrer une feuille complète

Dans cette étape, implémentez un contrôle indépendant de chaque manche enregistrée avant d’accepter un total. Continuez dans le même Terminal, dans /home/labex/project/dice-game ; répétez les commandes précédentes export PATH et cd au besoin. Conservez votre méthode submit terminée.

Vérifiez d’abord à la main la feuille fixe proposée : pair sur [2, 2, 4, 4, 6] donne 8, straight sur [1, 2, 3, 4, 6] donne 0 et sum sur [6, 5, 4, 3, 2] donne 20. Le total recalculé vaut 28. Si quelqu’un écrivait 10 au lieu de 8 dans la première ligne, additionner les points stockés masquerait l’enregistrement incorrect. Une feuille ne peut pas se prouver avec son propre total ou ses points stockés.

replay reçoit &[Round], un slice : une vue empruntée des enregistrements sans transfert de propriété ni modification. Concevez les contrôles avant d’écrire. Exigez exactement trois enregistrements. Pour chaque enregistrement, refusez les valeurs hors de 1 à 6 avant d’appeler score, car une implémentation peut utiliser une valeur comme indice de tableau. Suivez les catégories utilisées dans un petit Vec ; contains teste si la catégorie courante est déjà apparue. Refusez les doublons. Recalculez les points à partir des véritables dés et catégorie, puis comparez-les aux points stockés. Ajoutez uniquement le résultat recalculé au total, et ne renvoyez le total qu’après validation de tous les enregistrements.

Vous pouvez utiliser un return Err("...") précoce pour une condition échouée. Dans for round in rounds, chaque manche est empruntée ; round.dice est un petit tableau Copy examinable sans modifier l’enregistrement. La condition !(1..=6).contains(&face) signifie que la valeur est hors de l’intervalle valide inclusif. Choisissez des messages de refus explicites ; votre implémentation peut organiser ces décisions autrement en préservant le contrat.

Enregistrez un nouveau point de retour contenant votre submit déjà fonctionnel. Il ne doit pas être confondu avec la sauvegarde initiale précédente :

rs02_before_replay=$(mktemp -d .edit-before-replay.XXXXXX) && cp -p src/state.rs "$rs02_before_replay/state.rs"

Ouvrez le même fichier et recherchez pub fn replay avec Ctrl+W :

nano -c src/state.rs

Renommez le paramètre inutilisé _rounds en rounds, en gardant son type, et implémentez uniquement le corps de cette fonction. Laissez submit, les structs et les liaisons de modules en place. Utilisez vos contrôles ordonnés et votre accumulateur ; ne renvoyez pas simplement 28, ne faites pas confiance aux points stockés et n’ignorez pas silencieusement un enregistrement invalide. Enregistrez avec Ctrl+O, Enter, puis quittez avec Ctrl+X. Relisez le code enregistré :

cat src/state.rs

Relancez la séquence fixe :

cargo run --quiet -- play --fixture

Saisissez pair, puis straight, puis sum, en attendant chaque invite. La sortie finale comprend :

1 | [2, 2, 4, 4, 6] | pair | 8
2 | [1, 2, 3, 4, 6] | straight | 0
3 | [6, 5, 4, 3, 2] | sum | 20
Replayed total: 28
Saved match.tsv

L'interface préparée écrit match.tsv uniquement après qu'une feuille complète a passé replay. Chaque ligne contient cinq valeurs séparées par des espaces, une tabulation, la catégorie, une autre tabulation et les points. Ce petit format à tabulations est géré par l'interface préparée ; vous n'avez pas à écrire d'analyseur de fichiers. Chaque nouvelle partie terminée remplace cette feuille propre au cours. Une partie incomplète laisse la feuille précédente intacte.

Lisez et recalculez le véritable fichier enregistré :

cat match.tsv
cargo run --quiet -- replay match.tsv

Attendez les mêmes enregistrements et le total 28, sans Saved match.tsv. Replay lit le chemin exact sans le modifier ni utiliser un exemple de remplacement. Pour vérifier que vous n’avez pas inscrit en dur la séquence de catégories précédente dans submit, rejouez encore la séquence fixe :

cargo run --quiet -- play --fixture

Utilisez straight, sum, puis pair. Prévoyez 0, 16 et 0 : les trois choix sont légaux et le total recalculé vaut 16. Cette partie terminée remplace maintenant la feuille précédente. Relisez ce fichier courant :

cargo run --quiet -- replay match.tsv

Ses enregistrements doivent refléter cette deuxième séquence, pas l’ancienne partie à 28 points. Un total différent est attendu, car la règle choisie pour chaque lancer a changé. L’étape suivante utilise de véritables dés aléatoires avec les mêmes décisions.

Pour une erreur de compilation, rouvrez le fichier nommé et corrigez la ligne replay indiquée. Si nécessaire, restaurez votre propre point d’avant replay, qui conserve submit, puis réimplémentez replay :

Uniquement pour récupérer une erreur enregistrée : dans le Terminal d’origine, la variable du point de sauvegarde contient encore son chemin. Dans un nouveau Terminal, répétez d’abord les commandes PATH et de répertoire du projet. Énumérez les sauvegardes de cette phase :

ls -d .edit-before-replay.*

ls -d liste les noms de répertoires plutôt que leur contenu ; * correspond aux suffixes des répertoires uniques de cette phase. Ce sont de vrais noms générés : ne saisissez pas de suffixe inventé. Sans correspondance, confirmez le répertoire du projet et la création de ce point de sauvegarde ; ne copiez pas depuis une autre phase. Pour sélectionner l’un des chemins affichés, lancez :

read rs02_before_replay

Le shell attend une ligne. Collez le nom exact du répertoire choisi et appuyez sur Enter. read stocke cette ligne dans la variable nommée pour cette session. Confirmez le contenu de la sauvegarde avant de restaurer :

cat "$rs02_before_replay/state.rs"

Elle doit contenir le submit fonctionnel avant replay. Si le chemin ou la phase est incorrect, répétez read avec le bon chemin affiché et inspectez à nouveau. Utilisez seulement ensuite la commande de restauration. Ce contrôle vaut aussi dans le Terminal d’origine si vous ne savez plus quel point de sauvegarde sa variable désigne.

cp -p "$rs02_before_replay/state.rs" src/state.rs

La restauration du code source ne change pas une feuille existante. Une partie terminée ultérieure la remplace uniquement après réussite de replay ; les parties échouées et la lecture d’une feuille ne la changent pas. La référence complète ci-dessous sert à comparer une fonction précise après votre essai, pas à remplacer le module comme opération d’apprentissage normale.

Référence complète de l’état pour comparaison et récupération

Conservez vos champs et votre travail précédent réussi. Comparez uniquement la fonction à corriger, modifiez-la dans nano, puis lancez la séquence fixe et relisez la véritable feuille à nouveau.

use crate::{score, Category, Dice};

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Round {
    pub dice: Dice,
    pub category: Category,
    pub points: u16,
}

#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Game {
    pub current: Option<Dice>,
    pub rounds: Vec<Round>,
}

impl Game {
    pub fn new(first: Dice) -> Self {
        Self { current: Some(first), rounds: Vec::new() }
    }

    pub fn submit(&mut self, input: &str) -> Result<u16, &'static str> {
        let dice = self.current.ok_or("match finished")?;
        let category = Category::parse(input).ok_or("unknown category")?;
        for round in &self.rounds {
            if round.category == category {
                return Err("category already used");
            }
        }
        let points = score(dice, category);
        self.rounds.push(Round { dice, category, points });
        self.current = None;
        Ok(points)
    }
}

pub fn replay(rounds: &[Round]) -> Result<u16, &'static str> {
    if rounds.len() != 3 {
        return Err("card needs three rounds");
    }
    let mut used = Vec::new();
    let mut total = 0;
    for round in rounds {
        for face in round.dice {
            if !(1..=6).contains(&face) {
                return Err("card has an invalid face");
            }
        }
        if used.contains(&round.category) {
            return Err("card repeats a category");
        }
        let expected = score(round.dice, round.category);
        if expected != round.points {
            return Err("card has incorrect points");
        }
        used.push(round.category);
        total += expected;
    }
    Ok(total)
}

Jouer et réutiliser votre propre partie

Dans cette étape, vous allez utiliser les règles terminées avec de nouveaux dés et corriger une erreur courante de chemin de fichier. Continuez dans le même Terminal, dans /home/labex/project/dice-game.

Sans --fixture, l'interface préparée lit des octets aléatoires depuis /dev/urandom. Elle produit cinq valeurs de 1 à 6 par lancer. Il s'agit d'un jeu local d'entraînement. Vous n'avez pas à atteindre un total donné ou à adopter une stratégie gagnante ; les points doivent respecter les règles. Chacune des catégories sum, pair et straight doit être utilisée une fois, dans n'importe quel ordre. Les mots refusés et les catégories répétées conservent le lancer courant.

Commencez une nouvelle partie :

cargo run --quiet -- play

Lisez les premiers dés, choisissez une catégorie inutilisée et appuyez sur Enter. Répétez aux deuxième et troisième manches. Vous pouvez saisir volontairement pairs une fois pour observer un refus, puis corriger avec une catégorie disponible. Remarquez que les dés et le compteur rolls ne changent pas pendant la correction. Après trois choix acceptés, examinez les trois enregistrements et le total recalculé. Les valeurs aléatoires peuvent différer de la séquence fixe ; la partie terminée remplace match.tsv.

Relisez la nouvelle feuille :

cargo run --quiet -- replay match.tsv

Les enregistrements doivent correspondre à votre dernière partie, zéros légitimes compris. Pour pratiquer une erreur de chemin courante, la commande suivante demande volontairement un fichier inexistant. Attendez-vous au message Error: missing-card.tsv: ... et au statut 1 ; aucune feuille de remplacement ne doit être produite :

cargo run --quiet -- replay missing-card.tsv

Corrigez en utilisant le vrai nom de fichier :

cargo run --quiet -- replay match.tsv

Votre feuille reste lisible et n'est modifiée par aucune des deux relectures. Conservez src/main.rs, Cargo.toml et l'API publique. La vérification exécute votre implémentation originale dans une copie isolée et lit votre véritable match.tsv ; elle ne crée pas de feuille à votre place.

Résumé

Vous avez modélisé une partie et ses enregistrements, renvoyé un succès ou un refus explicite et modifié l'état uniquement après validation. Les choix invalides ont conservé le lancer, les zéros légitimes ont fait avancer la partie et la relecture a recalculé une feuille complète. Vous avez joué avec de nouveaux dés et réutilisé le fichier réellement enregistré sans dépendre d'une VM précédente.