Salta ai contenuti

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.

Alla fine, una partita sul tuo computer avrà questo aspetto:

Indovina il numero! (da 1 a 100)
Scrivi il tuo tentativo:
50
Troppo grande!
Scrivi il tuo tentativo:
ciao
Questo non è un numero intero. Riprova!
Scrivi il tuo tentativo:
25
Troppo piccolo!
Scrivi il tuo tentativo:
37
Indovinato 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.

Apri un terminale e crea un nuovo progetto Cargo:

Terminal window
cargo new guessing_game
cd guessing_game

Tutto il codice di questa pagina va in src/main.rs. Puoi eseguirlo quando vuoi con:

Terminal window
cargo run

Se non hai ancora installato Rust, guarda prima Primi passi.

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 a guess. Ha bisogno di &mut perché modifica la stringa. Guarda Prestito (borrowing) se &mut è una novità per te.
  • read_line restituisce un Result, 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.

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)
▶ Provalo tu

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!
▶ Provalo tu

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)
> 50
Troppo grande!
> ciao
Questo non è un numero intero. Riprova!
> 25
Troppo piccolo!
> 37
Indovinato in 3 tentativi!
▶ Provalo tu

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).

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:

Terminal window
cargo add rand

Per 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? true
▶ Provalo tu

nanos % 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.

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. 🦀

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 incorrect

Il 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)
▶ Provalo tu

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. Esegui cargo add rand e segui la sua documentazione su docs.rs (in inglese) per sostituire secret_number().
  • Sposta la logica in funzioni. Scrivi fn check_guess(guess: u32, secret: u32) -> Ordering e aggiungi dei test.
  • io::stdin().read_line(&mut s) legge una riga, compreso l’a capo, e restituisce un Result. Ok(0) significa che non c’è più input.
  • .trim().parse() trasforma il testo in un numero. Restituisce un Result, quindi fai match per gestire con gentilezza l’input sbagliato.
  • a.cmp(&b) restituisce Ordering::Less, Greater o Equal, e match si assicura che tu li gestisca tutti e tre.
  • loop con continue e break fa 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

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