Progetto: gioco «indovina il numero» in Rust
Questo è il tuo primo progetto vero. Il computer sceglie un numero segreto tra 1 e 100. Tu provi a indovinarlo. Lui ti dice «troppo piccolo» o «troppo grande» finché non ci azzecchi, e poi ti dice quanti tentativi ti sono serviti.
È un programma piccolo, ma usa tanto di quello che hai imparato finora: variabili, match, cicli e Result. Lo costruiamo un passo alla volta, e ogni passo funziona anche da solo.
Cosa costruirai
Sezione intitolata “Cosa costruirai”Alla fine, una partita sul tuo computer avrà questo aspetto:
Indovina il numero! (da 1 a 100)Scrivi il tuo tentativo:50Troppo grande!Scrivi il tuo tentativo:ciaoQuesto non è un numero intero. Riprova!Scrivi il tuo tentativo:25Troppo piccolo!Scrivi il tuo tentativo:37Indovinato in 3 tentativi!Nota due dettagli gentili. Quando il giocatore scrive ciao, il gioco non va in crash. Chiede di nuovo. E quell’input sbagliato non conta come tentativo.
Passo 1: crea il progetto
Sezione intitolata “Passo 1: crea il progetto”Apri un terminale e crea un nuovo progetto Cargo:
cargo new guessing_gamecd guessing_gameTutto il codice di questa pagina va in src/main.rs. Puoi eseguirlo quando vuoi con:
cargo runSe non hai ancora installato Rust, guarda prima Primi passi.
Passo 2: leggi un tentativo dalla tastiera
Sezione intitolata “Passo 2: leggi un tentativo dalla tastiera”Per leggere quello che scrive il giocatore, usa std::io::stdin() e il suo metodo read_line:
use std::io;
fn main() { println!("Scrivi il tuo tentativo:");
let mut guess = String::new(); io::stdin() .read_line(&mut guess) .expect("Impossibile leggere la riga");
println!("Hai scritto: {guess}");}Questo programma aspetta che tu scriva qualcosa e prema Invio, quindi eseguilo con cargo run sul tuo computer. (Il Rust Playground non ha l’input da tastiera, ed è per questo che qui non c’è il pulsante «Provalo tu».)
Succedono alcune cose:
String::new()crea una stringa vuota e espandibile che conterrà il testo.read_line(&mut guess)aggiunge la riga scritta in fondo aguess. Ha bisogno di&mutperché modifica la stringa. Guarda Prestito (borrowing) se&mutè una novità per te.read_linerestituisce unResult, perché leggere l’input può fallire..expect(...)dice «se fallisce, ferma il programma con questo messaggio». Per ora va bene; più avanti lo gestiremo con più garbo.
Una sorpresa: il testo include il tasto Invio. Se scrivi 42, guess contiene "42\n". Tienilo a mente per il prossimo passo.
Passo 3: trasforma il testo in un numero
Sezione intitolata “Passo 3: trasforma il testo in un numero”Il giocatore ha scritto del testo, ma noi vogliamo un numero da confrontare. .trim() toglie gli spazi e l’a capo a entrambe le estremità. Poi .parse() prova a trasformare il testo in un numero.
Il parsing può fallire. "sette" non è un numero. Quindi parse() restituisce un Result, e usiamo match per gestire entrambi i casi:
fn main() { // Facciamo finta che il giocatore abbia scritto queste righe (ognuna finisce con Invio). let inputs = ["42\n", " 7 \n", "sette\n", "-3\n"];
for input in inputs { match input.trim().parse::<u32>() { Ok(num) => println!("{:?} -> il numero {num}", input), Err(e) => println!("{:?} -> tentativo non valido ({e})", input), } }}Output:
"42\n" -> il numero 42" 7 \n" -> il numero 7"sette\n" -> tentativo non valido (invalid digit found in string)"-3\n" -> tentativo non valido (invalid digit found in string)Il messaggio tra parentesi è l’errore di Rust, che resta in inglese: «trovata una cifra non valida nella stringa». parse::<u32>() dice a Rust in quale tipo convertire. u32 è un numero intero senza segno (mai negativo), ed è per questo che -3 viene rifiutato. È proprio quello che vogliamo: il numero segreto è sempre tra 1 e 100.
Nel gioco scriviamo la stessa idea così:
let guess: u32 = match guess.trim().parse() { Ok(num) => num, Err(_) => { println!("Questo non è un numero intero. Riprova!"); continue; }};Qui il tipo viene da let guess: u32, quindi non serve ::<u32>. Se il parsing riesce, il match restituisce il numero. Se fallisce, stampiamo un messaggio gentile e facciamo continue per chiedere di nuovo. (continue funziona solo dentro un ciclo, che aggiungiamo al Passo 5.)
Perché non usare semplicemente .expect()? Perché così un errore di battitura farebbe crashare tutto il gioco. Gestire il caso Err è il modo per scrivere un programma piacevole da usare.
Passo 4: confronta il tentativo con il numero segreto
Sezione intitolata “Passo 4: confronta il tentativo con il numero segreto”Ogni numero ha un metodo .cmp(). Confronta due valori e restituisce un std::cmp::Ordering, un enum con tre varianti: Less (minore), Greater (maggiore) ed Equal (uguale). Un match su di esso si legge quasi come una frase:
use std::cmp::Ordering;
fn main() { let secret: u32 = 42;
for guess in [10, 42, 99] { match guess.cmp(&secret) { Ordering::Less => println!("{guess}: Troppo piccolo!"), Ordering::Greater => println!("{guess}: Troppo grande!"), Ordering::Equal => println!("{guess}: Hai vinto!"), } }}Output:
10: Troppo piccolo!42: Hai vinto!99: Troppo grande!Potresti scrivere la stessa cosa con if guess < secret ... else if .... La versione con match è comoda perché il compilatore controlla che tu abbia gestito tutti e tre i casi. Se ne dimentichi uno, non compila.
Passo 5: continua a chiedere finché il tentativo è giusto
Sezione intitolata “Passo 5: continua a chiedere finché il tentativo è giusto”Ora avvolgiamo tutto in un loop. Gira all’infinito finché non ne usciamo con break. Teniamo anche un contatore, attempts, e aggiungiamo 1 solo quando il tentativo era un numero vero.
Nel gioco vero, l’input arriva dalla tastiera. Per fare una versione che giri nel Playground, usiamo un numero segreto fisso e una lista fissa di righe «scritte». inputs.next() ci dà la riga successiva, oppure None quando la lista è finita:
use std::cmp::Ordering;
fn main() { let secret: u32 = 37; // Facciamo finta che il giocatore abbia scritto queste, una per riga. let mut inputs = ["50", "ciao", "25", "37"].into_iter(); let mut attempts = 0;
println!("Indovina il numero! (da 1 a 100)");
loop { let input = match inputs.next() { Some(line) => line, None => { println!("Tentativi finiti. Il numero era {secret}."); break; } }; println!("> {input}");
let guess: u32 = match input.trim().parse() { Ok(num) => num, Err(_) => { println!("Questo non è un numero intero. Riprova!"); continue; } };
attempts += 1;
match guess.cmp(&secret) { Ordering::Less => println!("Troppo piccolo!"), Ordering::Greater => println!("Troppo grande!"), Ordering::Equal => { println!("Indovinato in {attempts} tentativi!"); break; } } }}Output:
Indovina il numero! (da 1 a 100)> 50Troppo grande!> ciaoQuesto non è un numero intero. Riprova!> 25Troppo piccolo!> 37Indovinato in 3 tentativi!Prova a cambiare il numero segreto o la lista dei tentativi ed eseguilo di nuovo. Metti la risposta giusta per prima, oppure toglila, e guarda cosa succede.
Il caso None è importante. È la versione Playground di «il giocatore non ha più input», e il gioco vero ha bisogno della stessa idea (vedi il Passo 7).
Passo 6: scegli un numero segreto casuale
Sezione intitolata “Passo 6: scegli un numero segreto casuale”Un gioco con sempre lo stesso numero segreto non è molto divertente. In un progetto vero useresti il crate rand (in inglese), la scelta standard per i numeri casuali in Rust. La lezione Numeri casuali mostra come usarlo. Lo aggiungi con:
cargo add randPer questo progetto restiamo solo con la libreria standard, così non ti serve nessuna configurazione. Il trucco: chiedi all’orologio l’ora attuale, prendi la parte dei nanosecondi (un numero da 0 a 999.999.999 che cambia di continuo) e schiacciala nell’intervallo da 1 a 100 con % (il resto della divisione):
use std::time::{SystemTime, UNIX_EPOCH};
fn secret_number() -> u32 { let nanos = SystemTime::now() .duration_since(UNIX_EPOCH) .expect("l'orologio è impostato prima del 1970") .subsec_nanos(); nanos % 100 + 1}
fn main() { let secret = secret_number(); let in_range = (1..=100).contains(&secret); println!("Numero segreto scelto. È tra 1 e 100? {in_range}");}Output:
Numero segreto scelto. È tra 1 e 100? truenanos % 100 va sempre da 0 a 99, quindi aggiungendo 1 si ottiene da 1 a 100. Stampiamo solo se è nell’intervallo, così l’output è sempre lo stesso. Stampa secret se vuoi sbirciare.
Sii onesto con te stesso su cosa sia questo: è «abbastanza imprevedibile per un gioco», non vera casualità. Non usare mai un trucco del genere per password, token di sicurezza o qualsiasi cosa che qualcuno potrebbe provare a indovinare. Per quelle cose usa un crate adatto.
Passo 7: il gioco completo
Sezione intitolata “Passo 7: il gioco completo”Ecco tutto insieme. Questa è la versione da mettere in src/main.rs e da giocare con cargo run.
Una novità: facciamo match sul risultato di read_line invece di chiamare .expect(). read_line restituisce Ok(0) quando non c’è più input, per esempio se il giocatore preme Ctrl+D (Ctrl+Z e poi Invio su Windows), o se l’input arriva da un file tramite pipe. Senza quel controllo, il ciclo girerebbe all’infinito su un input vuoto.
use std::cmp::Ordering;use std::io;use std::time::{SystemTime, UNIX_EPOCH};
fn secret_number() -> u32 { let nanos = SystemTime::now() .duration_since(UNIX_EPOCH) .expect("l'orologio è impostato prima del 1970") .subsec_nanos(); nanos % 100 + 1}
fn main() { println!("Indovina il numero! (da 1 a 100)");
let secret = secret_number(); let mut attempts = 0;
loop { println!("Scrivi il tuo tentativo:");
let mut guess = String::new(); match io::stdin().read_line(&mut guess) { Ok(0) => { // Fine dell'input: non c'è altro da leggere. println!("Input finito. Il numero era {secret}. Ciao!"); break; } Ok(_) => {} Err(e) => { println!("Non riesco a leggere l'input: {e}"); break; } }
let guess: u32 = match guess.trim().parse() { Ok(num) => num, Err(_) => { println!("Questo non è un numero intero. Riprova!"); continue; } };
attempts += 1;
match guess.cmp(&secret) { Ordering::Less => println!("Troppo piccolo!"), Ordering::Greater => println!("Troppo grande!"), Ordering::Equal => { println!("Indovinato in {attempts} tentativi!"); break; } } }}Per provarlo invece nel browser, usa la versione del Passo 5, che gioca una lista fissa di tentativi. 🦀
Errori comuni
Sezione intitolata “Errori comuni”Dimenticare mut sulla stringa. read_line deve modificare la stringa, quindi deve essere mutabile:
use std::io;
fn main() { let guess = String::new(); io::stdin().read_line(&mut guess).expect("Failed to read line");}error[E0596]: cannot borrow `guess` as mutable, as it is not declared as mutable --> src/main.rs:5:27 |5 | io::stdin().read_line(&mut guess).expect("Failed to read line"); | ^^^^^^^^^^ cannot borrow as mutable |help: consider changing this to be mutable |4 | let mut guess = String::new(); | +++Il compilatore dice che non può prendere in prestito guess come mutabile, perché non è dichiarata mutabile. La soluzione è esattamente quella che suggerisce: let mut guess = String::new();.
Confrontare testo con un numero. Se ti dimentichi del parsing, guess è ancora una String, e non puoi confrontare una String con un u32:
use std::cmp::Ordering;
fn main() { let secret: u32 = 42; let guess = String::from("50");
match guess.cmp(&secret) { Ordering::Less => println!("Troppo piccolo!"), Ordering::Greater => println!("Troppo grande!"), Ordering::Equal => println!("Hai vinto!"), }}error[E0308]: mismatched types --> src/main.rs:7:21 |7 | match guess.cmp(&secret) { | --- ^^^^^^^ expected `&String`, found `&u32` | | | arguments to this method are incorrectIl compilatore si aspettava un &String e ha trovato un &u32. Rust non converte mai da solo tra testo e numeri. Prima fai il parsing, poi confronta.
Dimenticare .trim(). Questo compila, ma ogni tentativo viene rifiutato, perché l’a capo non è una cifra:
fn main() { let typed = "42\n"; println!("{:?}", typed.parse::<u32>()); println!("{:?}", typed.trim().parse::<u32>());}Output:
Err(ParseIntError { kind: InvalidDigit })Ok(42)Idee per ampliarlo
Sezione intitolata “Idee per ampliarlo”Quando il tuo gioco funziona, prova queste da solo. Ognuna è piccola:
- Controlla l’intervallo. Se il tentativo è 0 o sopra 100, scrivi «Scegli un numero da 1 a 100» e non contarlo.
- Limita i tentativi. Dai al giocatore 7 tentativi. Se li finisce, mostra il numero segreto e chiudi la partita.
- Gioca ancora. Quando il giocatore vince, chiedi «Giochi ancora? (s/n)» e inizia un nuovo round con un nuovo numero segreto.
- Tieni il record. Ricorda il minor numero di tentativi tra un round e l’altro e stampalo alla fine.
- Dai indizi più caldi. Scrivi «vicinissimo!» quando il tentativo dista al massimo 3 dal numero segreto.
guess.abs_diff(secret)ti dà la distanza tra due numeri. - Usa il vero crate
rand. Eseguicargo add rande segui la sua documentazione su docs.rs (in inglese) per sostituiresecret_number(). - Sposta la logica in funzioni. Scrivi
fn check_guess(guess: u32, secret: u32) -> Orderinge aggiungi dei test.
Riepilogo
Sezione intitolata “Riepilogo”io::stdin().read_line(&mut s)legge una riga, compreso l’a capo, e restituisce unResult.Ok(0)significa che non c’è più input..trim().parse()trasforma il testo in un numero. Restituisce unResult, quindi faimatchper gestire con gentilezza l’input sbagliato.a.cmp(&b)restituisceOrdering::Less,GreateroEqual, ematchsi assicura che tu li gestisca tutti e tre.loopconcontinueebreakfa andare avanti il gioco finché il giocatore non vince.- Il trucco dell’orologio va bene per un gioco, ma i progetti veri usano il crate
rand.
Verifica veloce
Sezione intitolata “Verifica veloce”Verifica veloce
1. Il giocatore scrive 42 e preme Invio. Cosa contiene la stringa letta da read_line?
2. Perché il gioco usa match su parse() invece di .expect()?
3. Cosa significa se read_line restituisce Ok(0)?
4. Quali valori può restituire guess.cmp(&secret)?
Punteggio: 0 / 4
Per approfondire
Sezione intitolata “Per approfondire”- Programming a Guessing Game — The Rust Book (in inglese): la versione di questo progetto nel libro ufficiale. Usa il crate
rande spiega ogni riga nel dettaglio. Stdin::read_line— documentazione di std (in inglese): perché il testo viene aggiunto alla tuaString, perché tiene l’a capo e perché restituisceOk(0)alla fine dell’input.str::parse— documentazione di std (in inglese): comeparsesceglie il tipo di destinazione dalla tua annotazione, e com’è fatto l’errore quando il testo non è un numero.std::cmp::Ordering— documentazione di std (in inglese): l’enumLess/Equal/Greaterrestituito dacmp, più funzioni utili comeis_ltereverse.- Il crate
rand— docs.rs (in inglese): il modo giusto per scegliere numeri casuali, quando sarai pronto ad aggiungere il tuo primo crate.