Ir al contenido

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.

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:
hola
Eso 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.

Abre una terminal y crea un proyecto nuevo con Cargo:

Terminal window
cargo new guessing_game
cd guessing_game

Todo el código de esta página va en src/main.rs. Puedes ejecutarlo cuando quieras con:

Terminal window
cargo run

Si todavía no tienes Rust instalado, mira primero Empezar con Rust.

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 de guess. Necesita &mut porque modifica la cadena. Si &mut es nuevo para ti, mira Borrowing.
  • read_line devuelve un Result, 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.

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)
▶ Pruébalo tú mismo

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!
▶ Pruébalo tú mismo

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.

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!
> hola
Eso no es un número entero. ¡Inténtalo de nuevo!
> 25
¡Demasiado pequeño!
> 37
¡Lo lograste en 3 intentos!
▶ Pruébalo tú mismo

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

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:

Terminal window
cargo add rand

En 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? true
▶ Pruébalo tú mismo

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

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

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)
▶ Pruébalo tú mismo

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 rand de verdad. Ejecuta cargo add rand y sigue su documentación en docs.rs (en inglés) para reemplazar secret_number().
  • Pasa la lógica a funciones. Escribe fn check_guess(guess: u32, secret: u32) -> Ordering y agrégale tests (pruebas).
  • io::stdin().read_line(&mut s) lee una línea, incluido el salto de línea, y devuelve un Result. Ok(0) significa que ya no hay más entrada.
  • .trim().parse() convierte texto en un número. Devuelve un Result, así que haz match sobre él para manejar con amabilidad la entrada inválida.
  • a.cmp(&b) devuelve Ordering::Less, Greater o Equal, y match se asegura de que manejes los tres casos.
  • loop con continue y break mantiene 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 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