Proyecto: Juego de adivinanzas en Rust
Este es tu primer proyecto de verdad. La computadora elige un número secreto entre 1 y 100. Tú intentas adivinarlo. El programa te dice “demasiado pequeño” o “demasiado grande” hasta que aciertes, y al final te dice cuántos intentos necesitaste.
Es un programa pequeño, pero usa mucho de lo que ya aprendiste: variables, match, bucles y Result. Lo vamos a construir paso a paso, y cada paso funciona por sí solo.
Lo que vas a construir
Sección titulada «Lo que vas a construir»Cuando termines, el juego en tu computadora se verá así:
¡Adivina el número! (del 1 al 100)Escribe tu intento:50¡Demasiado grande!Escribe tu intento:holaEso no es un número entero. ¡Inténtalo de nuevo!Escribe tu intento:25¡Demasiado pequeño!Escribe tu intento:37¡Lo lograste en 3 intentos!Fíjate en dos detalles amables. Cuando el jugador escribe hola, el juego no se rompe: vuelve a preguntar. Y esa entrada inválida no cuenta como intento.
Paso 1: Crea el proyecto
Sección titulada «Paso 1: Crea el proyecto»Abre una terminal y crea un proyecto nuevo con Cargo:
cargo new guessing_gamecd guessing_gameTodo el código de esta página va en src/main.rs. Puedes ejecutarlo cuando quieras con:
cargo runSi todavía no tienes Rust instalado, mira primero Empezar con Rust.
Paso 2: Lee un intento desde el teclado
Sección titulada «Paso 2: Lee un intento desde el teclado»Para leer lo que escribe el jugador, usa std::io::stdin() y su método read_line:
use std::io;
fn main() { println!("Escribe tu intento:");
let mut guess = String::new(); io::stdin() .read_line(&mut guess) .expect("No se pudo leer la línea");
println!("Tu intento: {guess}");}Este programa espera a que escribas algo y pulses Enter, así que ejecútalo con cargo run en tu propia máquina. (El Rust Playground no tiene entrada de teclado; por eso aquí no hay botón de “Pruébalo”).
Aquí pasan varias cosas:
String::new()crea una String vacía que puede crecer, para guardar el texto.read_line(&mut guess)agrega la línea escrita al final deguess. Necesita&mutporque modifica la cadena. Si&mutes nuevo para ti, mira Borrowing.read_linedevuelve unResult, porque leer la entrada puede fallar..expect(...)significa “si esto falla, detén el programa con este mensaje”. Por ahora está bien; más adelante lo manejaremos con más cuidado.
Una sorpresa: el texto incluye la tecla Enter. Si escribes 42, guess contiene "42\n". Tenlo en cuenta para el siguiente paso.
Paso 3: Convierte el texto en un número
Sección titulada «Paso 3: Convierte el texto en un número»El jugador escribió texto, pero nosotros queremos un número para poder compararlo. .trim() quita los espacios y el salto de línea de ambos extremos. Luego .parse() intenta convertir el texto en un número.
Convertir puede fallar: "siete" no es un número. Por eso parse() devuelve un Result, y usamos match para manejar los dos casos:
fn main() { // Imagina que el jugador escribió estas líneas (cada una termina con Enter). let inputs = ["42\n", " 7 \n", "siete\n", "-3\n"];
for input in inputs { match input.trim().parse::<u32>() { Ok(num) => println!("{:?} -> el número {num}", input), Err(e) => println!("{:?} -> no es un intento válido ({e})", input), } }}Salida:
"42\n" -> el número 42" 7 \n" -> el número 7"siete\n" -> no es un intento válido (invalid digit found in string)"-3\n" -> no es un intento válido (invalid digit found in string)parse::<u32>() le dice a Rust a qué tipo convertir. u32 es un número entero sin signo (nunca negativo), y por eso se rechaza -3. Eso es justo lo que queremos: el número secreto siempre está entre 1 y 100.
El mensaje entre paréntesis (invalid digit found in string, “se encontró un dígito no válido en la cadena”) lo genera la biblioteca estándar de Rust, por eso sale en inglés.
En el juego escribimos la misma idea así:
let guess: u32 = match guess.trim().parse() { Ok(num) => num, Err(_) => { println!("Eso no es un número entero. ¡Inténtalo de nuevo!"); continue; }};Aquí el tipo viene de let guess: u32, así que no hace falta ::<u32>. Si la conversión funciona, el match devuelve el número. Si falla, mostramos un mensaje amable y usamos continue para volver a preguntar. (continue solo funciona dentro de un bucle, que agregaremos en el Paso 5).
¿Por qué no usar .expect() y ya? Porque entonces un error al escribir haría que todo el juego se cerrara. Manejar el caso Err es lo que hace que un programa sea agradable de usar.
Paso 4: Compara el intento con el número secreto
Sección titulada «Paso 4: Compara el intento con el número secreto»Todos los números tienen un método .cmp(). Compara dos valores y devuelve un std::cmp::Ordering, un enum (enumeración) con tres variantes: Less (menor), Greater (mayor) y Equal (igual). Un match sobre él se lee casi como 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}: ¡Demasiado pequeño!"), Ordering::Greater => println!("{guess}: ¡Demasiado grande!"), Ordering::Equal => println!("{guess}: ¡Ganaste!"), } }}Salida:
10: ¡Demasiado pequeño!42: ¡Ganaste!99: ¡Demasiado grande!Podrías escribir lo mismo con if guess < secret ... else if .... La versión con match tiene una ventaja: el compilador comprueba que manejaste los tres casos. Si olvidas uno, no compila.
Paso 5: Sigue preguntando hasta acertar
Sección titulada «Paso 5: Sigue preguntando hasta acertar»Ahora envolvemos todo en un loop (bucle). Se repite para siempre hasta que salimos con break. También llevamos un contador, attempts, y solo le sumamos 1 cuando el intento era un número de verdad.
En el juego real, la entrada viene del teclado. Para tener una versión que funcione en el Playground, usamos un número secreto fijo y una lista fija de líneas “escritas”. inputs.next() nos da la siguiente línea, o None cuando la lista se acaba:
use std::cmp::Ordering;
fn main() { let secret: u32 = 37; // Imagina que el jugador escribió esto, una línea cada vez. let mut inputs = ["50", "hola", "25", "37"].into_iter(); let mut attempts = 0;
println!("¡Adivina el número! (del 1 al 100)");
loop { let input = match inputs.next() { Some(line) => line, None => { println!("No quedan más intentos. El número era {secret}."); break; } }; println!("> {input}");
let guess: u32 = match input.trim().parse() { Ok(num) => num, Err(_) => { println!("Eso no es un número entero. ¡Inténtalo de nuevo!"); continue; } };
attempts += 1;
match guess.cmp(&secret) { Ordering::Less => println!("¡Demasiado pequeño!"), Ordering::Greater => println!("¡Demasiado grande!"), Ordering::Equal => { println!("¡Lo lograste en {attempts} intentos!"); break; } } }}Salida:
¡Adivina el número! (del 1 al 100)> 50¡Demasiado grande!> holaEso no es un número entero. ¡Inténtalo de nuevo!> 25¡Demasiado pequeño!> 37¡Lo lograste en 3 intentos!Prueba a cambiar el número secreto o la lista de intentos y vuelve a ejecutarlo. Pon la respuesta correcta al principio, o quítala, y mira qué pasa.
El caso None es importante. Es la versión de Playground de “el jugador ya no tiene más entrada”, y el juego real necesita la misma idea (mira el Paso 7).
Paso 6: Elige un número secreto al azar
Sección titulada «Paso 6: Elige un número secreto al azar»Un juego con el mismo número secreto cada vez no es muy divertido. En un proyecto real usarías el crate (paquete de Rust) rand, que es la opción habitual para números aleatorios en Rust. La lección de números aleatorios explica cómo usarlo. Lo agregas con:
cargo add randEn este proyecto nos quedamos solo con la biblioteca estándar, así que no necesitas configurar nada. El truco: le pedimos la hora actual al reloj, tomamos la parte de los nanosegundos (un número de 0 a 999.999.999 que cambia todo el tiempo) y lo metemos en el rango de 1 a 100 con % (el resto de la división):
use std::time::{SystemTime, UNIX_EPOCH};
fn secret_number() -> u32 { let nanos = SystemTime::now() .duration_since(UNIX_EPOCH) .expect("el reloj marca una fecha anterior a 1970") .subsec_nanos(); nanos % 100 + 1}
fn main() { let secret = secret_number(); let in_range = (1..=100).contains(&secret); println!("Número secreto elegido. ¿Está entre 1 y 100? {in_range}");}Salida:
Número secreto elegido. ¿Está entre 1 y 100? truenanos % 100 siempre da entre 0 y 99, así que al sumar 1 obtienes entre 1 y 100. Solo mostramos si está dentro del rango, para que la salida sea siempre la misma. Si quieres espiar, imprime secret.
Sé honesto contigo mismo sobre lo que es esto: es “lo bastante impredecible para un juego”, no aleatoriedad de verdad. Nunca uses un truco así para contraseñas, tokens de seguridad ni nada donde alguien pueda intentar adivinar el valor. Para eso, usa un crate adecuado.
Paso 7: El juego completo
Sección titulada «Paso 7: El juego completo»Aquí está todo junto. Esta es la versión que debes poner en src/main.rs para jugar con cargo run.
Una novedad: hacemos match sobre el resultado de read_line en lugar de llamar a .expect(). read_line devuelve Ok(0) cuando ya no hay más entrada; por ejemplo, si el jugador pulsa Ctrl+D (Ctrl+Z y luego Enter en Windows), o si la entrada viene de un archivo. Sin esa comprobación, el bucle daría vueltas para siempre con una entrada vacía.
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("el reloj marca una fecha anterior a 1970") .subsec_nanos(); nanos % 100 + 1}
fn main() { println!("¡Adivina el número! (del 1 al 100)");
let secret = secret_number(); let mut attempts = 0;
loop { println!("Escribe tu intento:");
let mut guess = String::new(); match io::stdin().read_line(&mut guess) { Ok(0) => { // Fin de la entrada: no hay nada más que leer. println!("No hay más entrada. El número era {secret}. ¡Adiós!"); break; } Ok(_) => {} Err(e) => { println!("No se pudo leer lo que escribiste: {e}"); break; } }
let guess: u32 = match guess.trim().parse() { Ok(num) => num, Err(_) => { println!("Eso no es un número entero. ¡Inténtalo de nuevo!"); continue; } };
attempts += 1;
match guess.cmp(&secret) { Ordering::Less => println!("¡Demasiado pequeño!"), Ordering::Greater => println!("¡Demasiado grande!"), Ordering::Equal => { println!("¡Lo lograste en {attempts} intentos!"); break; } } }}Si prefieres probarlo en el navegador, usa la versión del Paso 5, que juega con una lista fija de intentos. 🦀
Errores comunes
Sección titulada «Errores comunes»Olvidar mut en la cadena. read_line necesita modificar la cadena, así que debe ser mutable:
use std::io;
fn main() { let guess = String::new(); io::stdin().read_line(&mut guess).expect("No se pudo leer la línea");}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("No se pudo leer la línea"); | ^^^^^^^^^^ cannot borrow as mutable |help: consider changing this to be mutable |4 | let mut guess = String::new(); | +++El compilador te dice que no puede tomar guess como mutable porque no la declaraste así. La solución es exactamente la que sugiere: let mut guess = String::new();.
Comparar texto con un número. Si olvidas convertir, guess sigue siendo una String, y no puedes comparar 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!("¡Demasiado pequeño!"), Ordering::Greater => println!("¡Demasiado grande!"), Ordering::Equal => println!("¡Ganaste!"), }}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” significa “los tipos no coinciden”. Rust nunca convierte entre texto y números por ti. Primero convierte, después compara.
Olvidar .trim(). Esto sí compila, pero rechaza todos los intentos, porque el salto de línea no es un dígito:
fn main() { let typed = "42\n"; println!("{:?}", typed.parse::<u32>()); println!("{:?}", typed.trim().parse::<u32>());}Salida:
Err(ParseIntError { kind: InvalidDigit })Ok(42)Ideas para ampliarlo
Sección titulada «Ideas para ampliarlo»Cuando tu juego funcione, prueba estas ideas por tu cuenta. Cada una es pequeña:
- Comprueba el rango. Si el intento es 0 o mayor que 100, di “Elige un número del 1 al 100” y no lo cuentes.
- Limita los intentos. Dale al jugador 7 oportunidades. Si se le acaban, muestra el número secreto y termina el juego.
- Jugar otra vez. Cuando el jugador gane, pregunta “¿Jugar otra vez? (s/n)” y empieza una ronda nueva con otro número secreto.
- Guarda el mejor puntaje. Recuerda la menor cantidad de intentos entre rondas y muéstrala al final.
- Da pistas más cálidas. Di “¡muy cerca!” cuando el intento esté a 3 o menos del número secreto.
guess.abs_diff(secret)te da la distancia entre dos números. - Usa el crate
randde verdad. Ejecutacargo add randy sigue su documentación en docs.rs (en inglés) para reemplazarsecret_number(). - Pasa la lógica a funciones. Escribe
fn check_guess(guess: u32, secret: u32) -> Orderingy agrégale tests (pruebas).
Resumen
Sección titulada «Resumen»io::stdin().read_line(&mut s)lee una línea, incluido el salto de línea, y devuelve unResult.Ok(0)significa que ya no hay más entrada..trim().parse()convierte texto en un número. Devuelve unResult, así que hazmatchsobre él para manejar con amabilidad la entrada inválida.a.cmp(&b)devuelveOrdering::Less,GreateroEqual, ymatchse asegura de que manejes los tres casos.loopconcontinueybreakmantiene el juego en marcha hasta que el jugador gana.- El truco del reloj sirve para un juego, pero los proyectos reales usan el crate
rand.
Comprueba lo que aprendiste
Sección titulada «Comprueba lo que aprendiste»Comprueba lo aprendido
1. El jugador escribe 42 y pulsa Enter. ¿Qué contiene la cadena que devuelve read_line?
2. ¿Por qué el juego usa match sobre parse() en lugar de .expect()?
3. ¿Qué significa que read_line devuelva Ok(0)?
4. ¿Qué valores puede devolver guess.cmp(&secret)?
Puntuación: 0 / 4
Para leer más
Sección titulada «Para leer más»- Programming a Guessing Game — The Rust Book (en inglés): la versión del propio libro de este proyecto. Usa el crate
randy explica cada línea en detalle. Stdin::read_line— documentación de std (en inglés): por qué el texto se añade al final de tuString, por qué conserva el salto de línea y por qué devuelveOk(0)cuando se acaba la entrada.str::parse— documentación de std (en inglés): cómoparseelige el tipo de destino a partir de tu anotación, y qué aspecto tiene el error cuando el texto no es un número.std::cmp::Ordering— documentación de std (en inglés): el enumLess/Equal/Greaterque devuelvecmp, y métodos de ayuda comois_ltyreverse.- El crate
rand— docs.rs (en inglés): la forma de verdad de elegir números al azar, para cuando estés listo para añadir tu primer crate.