Pular para o conteúdo

Projeto: Jogo de adivinhação em Rust

Este é o seu primeiro projeto de verdade. O computador escolhe um número secreto entre 1 e 100. Você tenta adivinhar. O programa diz “muito baixo” ou “muito alto” até você acertar e, no final, conta quantas tentativas você precisou.

É um programa pequeno, mas usa muita coisa que você já aprendeu: variáveis, match, loops e Result. Vamos construir tudo um passo de cada vez, e cada passo roda sozinho.

Quando terminar, o jogo no seu computador vai ficar assim:

Adivinhe o número! (de 1 a 100)
Digite seu palpite:
50
Muito alto!
Digite seu palpite:
olá
Isso não é um número inteiro. Tente de novo!
Digite seu palpite:
25
Muito baixo!
Digite seu palpite:
37
Você acertou em 3 tentativas!

Repare em dois detalhes simpáticos. Quando o jogador digita olá, o jogo não quebra: ele pergunta de novo. E essa entrada inválida não conta como tentativa.

Abra um terminal e crie um novo projeto com o Cargo:

Terminal window
cargo new guessing_game
cd guessing_game

Todo o código desta página vai em src/main.rs. Rode quando quiser com:

Terminal window
cargo run

Se você ainda não instalou o Rust, veja primeiro Começando com Rust.

Para ler o que o jogador digita, use std::io::stdin() e o método read_line:

use std::io;
fn main() {
println!("Digite seu palpite:");
let mut guess = String::new();
io::stdin()
.read_line(&mut guess)
.expect("Falha ao ler a linha");
println!("Seu palpite: {guess}");
}

Este programa espera você digitar algo e apertar Enter, então rode com cargo run na sua própria máquina. (O Rust Playground não tem entrada pelo teclado, por isso não há botão de “Experimente você mesmo” aqui.)

Algumas coisas estão acontecendo:

  • String::new() cria uma String vazia, que pode crescer, para guardar o texto.
  • read_line(&mut guess) acrescenta a linha digitada no final de guess. Ele precisa de &mut porque altera a string. Se &mut é novidade para você, veja Borrowing (empréstimo).
  • read_line retorna um Result, porque ler a entrada pode falhar. .expect(...) quer dizer “se isso falhar, pare o programa com esta mensagem”. Por enquanto está ótimo; mais adiante vamos tratar isso com mais cuidado.

Uma surpresa: o texto inclui a tecla Enter. Se você digitar 42, guess guarda "42\n". Lembre disso no próximo passo.

O jogador digitou texto, mas queremos um número para comparar. .trim() remove os espaços e a quebra de linha das duas pontas. Depois, .parse() tenta transformar o texto em número.

A conversão pode falhar. "sete" não é um número. Por isso parse() retorna um Result, e usamos match para tratar os dois casos:

fn main() {
// Imagine que o jogador digitou estas linhas (cada uma termina com Enter).
let inputs = ["42\n", " 7 \n", "sete\n", "-3\n"];
for input in inputs {
match input.trim().parse::<u32>() {
Ok(num) => println!("{:?} -> o número {num}", input),
Err(e) => println!("{:?} -> não é um palpite válido ({e})", input),
}
}
}

Saída:

"42\n" -> o número 42
" 7 \n" -> o número 7
"sete\n" -> não é um palpite válido (invalid digit found in string)
"-3\n" -> não é um palpite válido (invalid digit found in string)
▶ Experimente você mesmo

O texto entre parênteses, invalid digit found in string (“dígito inválido encontrado na string”), é a mensagem do próprio Rust, por isso aparece em inglês.

parse::<u32>() diz ao Rust para qual tipo converter. u32 é um número inteiro sem sinal (nunca negativo), e é por isso que -3 é rejeitado. É exatamente o que queremos: o número secreto está sempre entre 1 e 100.

No jogo, escrevemos a mesma ideia assim:

let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => {
println!("Isso não é um número inteiro. Tente de novo!");
continue;
}
};

Aqui o tipo vem de let guess: u32, então não precisamos do ::<u32>. Se a conversão der certo, o match devolve o número. Se falhar, mostramos uma mensagem gentil e usamos continue para perguntar de novo. (continue só funciona dentro de um loop, que vamos adicionar no Passo 5.)

Por que não usar .expect() e pronto? Porque aí um erro de digitação derrubaria o jogo inteiro. Tratar o caso Err é o que torna um programa agradável de usar.

Todo número tem um método .cmp(). Ele compara dois valores e retorna um std::cmp::Ordering, um enum com três variantes: Less (menor), Greater (maior) e Equal (igual). Um match sobre ele fica quase como uma 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}: Muito baixo!"),
Ordering::Greater => println!("{guess}: Muito alto!"),
Ordering::Equal => println!("{guess}: Você ganhou!"),
}
}
}

Saída:

10: Muito baixo!
42: Você ganhou!
99: Muito alto!
▶ Experimente você mesmo

Você poderia escrever a mesma coisa com if guess < secret ... else if .... A versão com match é boa porque o compilador confere se você tratou os três casos. Esqueça um e o código não compila.

Agora colocamos tudo dentro de um loop. Ele roda para sempre, até sairmos com break. Também guardamos um contador, attempts, e só somamos 1 quando o palpite for um número de verdade.

No jogo real, a entrada vem do teclado. Para ter uma versão que rode no Playground, usamos um número secreto fixo e uma lista fixa de linhas “digitadas”. inputs.next() nos dá a próxima linha, ou None quando a lista acaba:

use std::cmp::Ordering;
fn main() {
let secret: u32 = 37;
// Imagine que o jogador digitou isto, uma linha por vez.
let mut inputs = ["50", "olá", "25", "37"].into_iter();
let mut attempts = 0;
println!("Adivinhe o número! (de 1 a 100)");
loop {
let input = match inputs.next() {
Some(line) => line,
None => {
println!("Acabaram os palpites. O número era {secret}.");
break;
}
};
println!("> {input}");
let guess: u32 = match input.trim().parse() {
Ok(num) => num,
Err(_) => {
println!("Isso não é um número inteiro. Tente de novo!");
continue;
}
};
attempts += 1;
match guess.cmp(&secret) {
Ordering::Less => println!("Muito baixo!"),
Ordering::Greater => println!("Muito alto!"),
Ordering::Equal => {
println!("Você acertou em {attempts} tentativas!");
break;
}
}
}
}

Saída:

Adivinhe o número! (de 1 a 100)
> 50
Muito alto!
> olá
Isso não é um número inteiro. Tente de novo!
> 25
Muito baixo!
> 37
Você acertou em 3 tentativas!
▶ Experimente você mesmo

Experimente mudar o número secreto ou a lista de palpites e rode de novo. Coloque a resposta certa em primeiro lugar, ou tire ela da lista, e veja o que acontece.

O caso None é importante. Ele é a versão de Playground para “o jogador não tem mais entrada”, e o jogo real precisa da mesma ideia (veja o Passo 7).

Um jogo com o mesmo número secreto toda vez não tem muita graça. Num projeto real, você usaria o crate rand (em inglês), que é a escolha padrão para números aleatórios em Rust. A lição Números aleatórios mostra como usá-lo. Para adicioná-lo:

Terminal window
cargo add rand

Neste projeto vamos ficar só com a biblioteca padrão, para você não precisar configurar nada. O truque: pedir ao relógio a hora atual, pegar a parte dos nanossegundos (um número de 0 a 999.999.999 que muda o tempo todo) e encaixá-lo no intervalo de 1 a 100 com % (resto da divisão):

use std::time::{SystemTime, UNIX_EPOCH};
fn secret_number() -> u32 {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("o relógio está ajustado antes de 1970")
.subsec_nanos();
nanos % 100 + 1
}
fn main() {
let secret = secret_number();
let in_range = (1..=100).contains(&secret);
println!("Número secreto escolhido. Está entre 1 e 100? {in_range}");
}

Saída:

Número secreto escolhido. Está entre 1 e 100? true
▶ Experimente você mesmo

nanos % 100 sempre dá de 0 a 99, então somando 1 temos de 1 a 100. Mostramos só se o número está no intervalo, para que a saída seja sempre a mesma. Se quiser espiar, imprima secret.

Seja honesto consigo mesmo sobre o que é isso: é “imprevisível o bastante para um jogo”, não aleatoriedade de verdade. Nunca use um truque assim para senhas, tokens de segurança ou qualquer coisa que alguém possa tentar adivinhar. Para isso, use um crate apropriado.

Aqui está tudo junto. Esta é a versão para colocar em src/main.rs e jogar com cargo run.

Uma novidade: fazemos match no resultado de read_line em vez de chamar .expect(). read_line retorna Ok(0) quando não há mais entrada, por exemplo se o jogador apertar Ctrl+D (Ctrl+Z e depois Enter no Windows), ou se a entrada vier de um arquivo. Sem essa verificação, o loop ficaria girando para sempre com uma entrada vazia.

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("o relógio está ajustado antes de 1970")
.subsec_nanos();
nanos % 100 + 1
}
fn main() {
println!("Adivinhe o número! (de 1 a 100)");
let secret = secret_number();
let mut attempts = 0;
loop {
println!("Digite seu palpite:");
let mut guess = String::new();
match io::stdin().read_line(&mut guess) {
Ok(0) => {
// Fim da entrada: não há mais nada para ler.
println!("Não há mais entrada. O número era {secret}. Tchau!");
break;
}
Ok(_) => {}
Err(e) => {
println!("Não foi possível ler o que você digitou: {e}");
break;
}
}
let guess: u32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => {
println!("Isso não é um número inteiro. Tente de novo!");
continue;
}
};
attempts += 1;
match guess.cmp(&secret) {
Ordering::Less => println!("Muito baixo!"),
Ordering::Greater => println!("Muito alto!"),
Ordering::Equal => {
println!("Você acertou em {attempts} tentativas!");
break;
}
}
}
}

Se preferir testar no navegador, use a versão do Passo 5, que joga com uma lista fixa de palpites. 🦀

Esquecer o mut na string. read_line precisa alterar a string, então ela tem que ser mutável:

use std::io;
fn main() {
let guess = String::new();
io::stdin().read_line(&mut guess).expect("Falha ao ler a linha");
}
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("Falha ao ler a linha");
| ^^^^^^^^^^ cannot borrow as mutable
|
help: consider changing this to be mutable
|
4 | let mut guess = String::new();
| +++

O compilador diz que não pode emprestar guess como mutável, porque ela não foi declarada assim. A correção é exatamente o que ele sugere: let mut guess = String::new();.

Comparar texto com número. Se você esquecer de converter, guess continua sendo uma String, e não dá para comparar uma String com um u32:

use std::cmp::Ordering;
fn main() {
let secret: u32 = 42;
let guess = String::from("50");
match guess.cmp(&secret) {
Ordering::Less => println!("Muito baixo!"),
Ordering::Greater => println!("Muito alto!"),
Ordering::Equal => println!("Você ganhou!"),
}
}
error[E0308]: mismatched types
--> src/main.rs:7:21
|
7 | match guess.cmp(&secret) {
| --- ^^^^^^^ expected `&String`, found `&u32`
| |
| arguments to this method are incorrect

“Mismatched types” quer dizer “os tipos não batem”: o método esperava uma &String e recebeu um &u32. O Rust nunca converte entre texto e números por você. Primeiro converta, depois compare.

Esquecer o .trim(). Este compila, mas todo palpite é rejeitado, porque a quebra de linha não é um dígito:

fn main() {
let typed = "42\n";
println!("{:?}", typed.parse::<u32>());
println!("{:?}", typed.trim().parse::<u32>());
}

Saída:

Err(ParseIntError { kind: InvalidDigit })
Ok(42)
▶ Experimente você mesmo

InvalidDigit é o jeito do Rust de dizer “dígito inválido”: sem o .trim(), o \n no final estraga a conversão.

Quando o seu jogo estiver funcionando, tente estas ideias por conta própria. Cada uma é pequena:

  • Confira o intervalo. Se o palpite for 0 ou maior que 100, diga “Escolha um número de 1 a 100” e não conte a tentativa.
  • Limite as tentativas. Dê 7 chances ao jogador. Se acabarem, mostre o número secreto e termine o jogo.
  • Jogue de novo. Quando o jogador ganhar, pergunte “Jogar de novo? (s/n)” e comece uma nova rodada com um novo número secreto.
  • Guarde o recorde. Lembre o menor número de tentativas entre as rodadas e mostre no final.
  • Dê dicas mais quentes. Diga “quase lá!” quando o palpite estiver a 3 ou menos do número secreto. guess.abs_diff(secret) dá a distância entre dois números.
  • Use o crate rand de verdade. Rode cargo add rand e siga a documentação dele no docs.rs (em inglês) para substituir secret_number().
  • Separe a lógica em funções. Escreva fn check_guess(guess: u32, secret: u32) -> Ordering e adicione testes para ela.
  • io::stdin().read_line(&mut s) lê uma linha, incluindo a quebra de linha, e retorna um Result. Ok(0) significa que não há mais entrada.
  • .trim().parse() transforma texto em número. Ele retorna um Result, então use match para tratar entradas inválidas com gentileza.
  • a.cmp(&b) retorna Ordering::Less, Greater ou Equal, e o match garante que você trate os três.
  • loop com continue e break mantém o jogo rodando até o jogador ganhar.
  • O truque do relógio serve para um jogo, mas projetos reais usam o crate rand.

Teste o que aprendeu

1. O jogador digita 42 e aperta Enter. O que a string lida por read_line contém?

2. Por que o jogo usa match em parse() em vez de .expect()?

3. O que significa read_line retornar Ok(0)?

4. Quais valores guess.cmp(&secret) pode retornar?

Pontuação: 0 / 4