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.
O que você vai construir
Seção intitulada “O que você vai construir”Quando terminar, o jogo no seu computador vai ficar assim:
Adivinhe o número! (de 1 a 100)Digite seu palpite:50Muito alto!Digite seu palpite:oláIsso não é um número inteiro. Tente de novo!Digite seu palpite:25Muito baixo!Digite seu palpite:37Você 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.
Passo 1: Crie o projeto
Seção intitulada “Passo 1: Crie o projeto”Abra um terminal e crie um novo projeto com o Cargo:
cargo new guessing_gamecd guessing_gameTodo o código desta página vai em src/main.rs. Rode quando quiser com:
cargo runSe você ainda não instalou o Rust, veja primeiro Começando com Rust.
Passo 2: Leia um palpite do teclado
Seção intitulada “Passo 2: Leia um palpite do teclado”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 deguess. Ele precisa de&mutporque altera a string. Se&muté novidade para você, veja Borrowing (empréstimo).read_lineretorna umResult, 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.
Passo 3: Transforme o texto em número
Seção intitulada “Passo 3: Transforme o texto em número”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)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.
Passo 4: Compare o palpite com o número secreto
Seção intitulada “Passo 4: Compare o palpite com o número secreto”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!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.
Passo 5: Continue perguntando até acertar
Seção intitulada “Passo 5: Continue perguntando até acertar”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)> 50Muito alto!> oláIsso não é um número inteiro. Tente de novo!> 25Muito baixo!> 37Você acertou em 3 tentativas!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).
Passo 6: Sorteie um número secreto
Seção intitulada “Passo 6: Sorteie um número secreto”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:
cargo add randNeste 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? truenanos % 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.
Passo 7: O jogo completo
Seção intitulada “Passo 7: O jogo completo”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. 🦀
Erros comuns
Seção intitulada “Erros comuns”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)InvalidDigit é o jeito do Rust de dizer “dígito inválido”: sem o .trim(), o \n no final estraga a conversão.
Ideias para ir além
Seção intitulada “Ideias para ir além”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
randde verdade. Rodecargo add rande siga a documentação dele no docs.rs (em inglês) para substituirsecret_number(). - Separe a lógica em funções. Escreva
fn check_guess(guess: u32, secret: u32) -> Orderinge adicione testes para ela.
io::stdin().read_line(&mut s)lê uma linha, incluindo a quebra de linha, e retorna umResult.Ok(0)significa que não há mais entrada..trim().parse()transforma texto em número. Ele retorna umResult, então usematchpara tratar entradas inválidas com gentileza.a.cmp(&b)retornaOrdering::Less,GreaterouEqual, e omatchgarante que você trate os três.loopcomcontinueebreakmanté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
Seção intitulada “Teste o que aprendeu”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
Para saber mais
Seção intitulada “Para saber mais”- Programming a Guessing Game — The Rust Book (em inglês): a versão do próprio Book para este projeto. Ela usa o crate
rande explica cada linha em detalhes. Stdin::read_line— documentação da std (em inglês): por que o texto é adicionado ao final da suaString, por que ele mantém a quebra de linha e por que retornaOk(0)no fim da entrada.str::parse— documentação da std (em inglês): como oparseescolhe o tipo de destino a partir da sua anotação e como é o erro quando o texto não é um número.std::cmp::Ordering— documentação da std (em inglês): o enumLess/Equal/Greaterque ocmpretorna, além de funções auxiliares comois_ltereverse.- O crate
rand— docs.rs (em inglês): o jeito de verdade de sortear números aleatórios, quando você estiver pronto para adicionar seu primeiro crate.