Programación de un juego de adivinanza

RustRustBeginner
Practicar Ahora

This tutorial is from open-source community. Access the source code

💡 Este tutorial está traducido por IA desde la versión en inglés. Para ver la versión original, puedes hacer clic aquí

Introducción

Bienvenido a Programando un juego de adivinanza. Esta práctica es parte del Rust Book. Puedes practicar tus habilidades de Rust en LabEx.

En esta práctica, implementaremos un juego de adivinanza en Rust, donde el programa genera un número aleatorio y pide al jugador que lo adivine, proporcionando retroalimentación sobre si la suposición es demasiado baja o demasiado alta, y felicitando al jugador si adivina correctamente.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL rust(("Rust")) -.-> rust/BasicConceptsGroup(["Basic Concepts"]) rust(("Rust")) -.-> rust/DataTypesGroup(["Data Types"]) rust(("Rust")) -.-> rust/FunctionsandClosuresGroup(["Functions and Closures"]) rust(("Rust")) -.-> rust/DataStructuresandEnumsGroup(["Data Structures and Enums"]) rust/BasicConceptsGroup -.-> rust/variable_declarations("Variable Declarations") rust/BasicConceptsGroup -.-> rust/mutable_variables("Mutable Variables") rust/DataTypesGroup -.-> rust/integer_types("Integer Types") rust/DataTypesGroup -.-> rust/string_type("String Type") rust/FunctionsandClosuresGroup -.-> rust/function_syntax("Function Syntax") rust/FunctionsandClosuresGroup -.-> rust/expressions_statements("Expressions and Statements") rust/DataStructuresandEnumsGroup -.-> rust/method_syntax("Method Syntax") subgraph Lab Skills rust/variable_declarations -.-> lab-100386{{"Programación de un juego de adivinanza"}} rust/mutable_variables -.-> lab-100386{{"Programación de un juego de adivinanza"}} rust/integer_types -.-> lab-100386{{"Programación de un juego de adivinanza"}} rust/string_type -.-> lab-100386{{"Programación de un juego de adivinanza"}} rust/function_syntax -.-> lab-100386{{"Programación de un juego de adivinanza"}} rust/expressions_statements -.-> lab-100386{{"Programación de un juego de adivinanza"}} rust/method_syntax -.-> lab-100386{{"Programación de un juego de adivinanza"}} end

Programando un juego de adivinanza

¡Vamos a sumergirnos en Rust trabajando juntos en un proyecto práctico! En este capítulo, te presento algunos conceptos comunes de Rust mostrándote cómo utilizarlos en un programa real. Aprenderás sobre let, match, métodos, funciones asociadas, cajas externas y mucho más. En los siguientes capítulos, exploraremos estas ideas con más detalle. En este capítulo, solo practicarás los fundamentos.

Implementaremos un problema de programación clásico para principiantes: un juego de adivinanza. Aquí está cómo funciona: el programa generará un número entero aleatorio entre 1 y 100. Luego, pedirá al jugador que ingrese una suposición. Después de que se ingrese una suposición, el programa indicará si la suposición es demasiado baja o demasiado alta. Si la suposición es correcta, el juego imprimirá un mensaje de felicitación y saldrá.

Configurando un nuevo proyecto

Para configurar un nuevo proyecto, ve al directorio proyecto que creaste en el Capítulo 1 y crea un nuevo proyecto usando Cargo, así:

cargo new guessing_game
cd guessing_game

El primer comando, cargo new, toma el nombre del proyecto (guessing_game) como primer argumento. El segundo comando cambia al directorio del nuevo proyecto.

Echa un vistazo al archivo Cargo.toml generado:

Nombre del archivo: Cargo.toml

[package]
name = "guessing_game"
version = "0.1.0"
edition = "2021"

## Ver más claves y sus definiciones en
https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

Como viste en el Capítulo 1, cargo new genera un programa "Hola, mundo!" para ti. Echa un vistazo al archivo src/main.rs:

Nombre del archivo: src/main.rs

fn main() {
    println!("Hello, world!");
}

Ahora vamos a compilar este programa "Hola, mundo!" y ejecutarlo en un solo paso usando el comando cargo run:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 1.50s
     Running `target/debug/guessing_game`
Hello, world!

El comando run es muy útil cuando necesitas iterar rápidamente en un proyecto, como lo haremos en este juego, probando rápidamente cada iteración antes de pasar a la siguiente.

Vuelve a abrir el archivo src/main.rs. Escribirás todo el código en este archivo.

Procesando una suposición

La primera parte del programa del juego de adivinanza solicitará la entrada del usuario, procesará esa entrada y comprobará que la entrada tenga el formato esperado. Para comenzar, permitiremos que el jugador ingrese una suposición. Ingresa el código de la Lista 2-1 en src/main.rs.

Nombre del archivo: src/main.rs

use std::io;

fn main() {
    println!("Adivina el número!");

    println!("Por favor, ingresa tu suposición.");

    let mut guess = String::new();

    io::stdin()
     .read_line(&mut guess)
     .expect("Falló al leer la línea");

    println!("Has adivinado: {guess}");
}

Lista 2-1: Código que obtiene una suposición del usuario y la imprime

Este código contiene mucha información, así que repasemoslo línea por línea. Para obtener la entrada del usuario y luego imprimir el resultado como salida, necesitamos traer la librería de entrada/salida io al ámbito. La librería io proviene de la librería estándar, conocida como std:

use std::io;

Por defecto, Rust tiene un conjunto de elementos definidos en la librería estándar que trae al ámbito de cada programa. Este conjunto se llama preámbulo, y puedes ver todo lo que contiene en https://doc.rust-lang.org/std/prelude/index.html.

Si un tipo que quieres usar no está en el preámbulo, debes traer ese tipo al ámbito explícitamente con una declaración use. Usar la librería std::io te proporciona una serie de características útiles, incluyendo la capacidad de aceptar la entrada del usuario.

Como viste en el Capítulo 1, la función main es el punto de entrada del programa:

fn main() {

La sintaxis fn declara una nueva función; los paréntesis, (), indican que no hay parámetros; y la llave curva, {, inicia el cuerpo de la función.

Como también aprendiste en el Capítulo 1, println! es una macro que imprime una cadena en la pantalla:

println!("Adivina el número!");

println!("Por favor, ingresa tu suposición.");

Este código está imprimiendo un mensaje de solicitud que indica de qué se trata el juego y solicita la entrada del usuario.

Almacenando valores con variables

A continuación, crearemos una variable para almacenar la entrada del usuario, así:

let mut guess = String::new();

¡Ahora el programa está resultando interesante! Hay muchas cosas sucediendo en esta pequeña línea. Usamos la declaración let para crear la variable. Aquí hay otro ejemplo:

let apples = 5;

Esta línea crea una nueva variable llamada apples y la asocia con el valor 5. En Rust, las variables son inmutables por defecto, lo que significa que una vez que le damos un valor a una variable, ese valor no cambiará. Vamos a discutir este concepto en detalle en "Variables y Mutabilidad". Para hacer una variable mutable, agregamos mut antes del nombre de la variable:

let apples = 5; // inmutable
let mut bananas = 5; // mutable

Nota: La sintaxis // inicia un comentario que continúa hasta el final de la línea. Rust ignora todo lo que está en los comentarios. Vamos a discutir los comentarios con más detalle en el Capítulo 3.

Volviendo al programa del juego de adivinanza, ahora sabes que let mut guess introducirá una variable mutable llamada guess. El signo igual (=) le dice a Rust que queremos asociar algo a la variable ahora. A la derecha del signo igual está el valor al que guess está asociado, que es el resultado de llamar a String::new, una función que devuelve una nueva instancia de un String. String es un tipo de cadena proporcionado por la librería estándar que es un trozo de texto codificado en UTF-8 y que puede crecer.

La sintaxis :: en la línea ::new indica que new es una función asociada del tipo String. Una función asociada es una función que se implementa en un tipo, en este caso String. Esta función new crea una nueva cadena vacía. Encontrarás una función new en muchos tipos porque es un nombre común para una función que crea un nuevo valor de algún tipo.

En resumen, la línea let mut guess = String::new(); ha creado una variable mutable que actualmente está asociada a una nueva instancia vacía de un String. ¡Puff!

Recibiendo la entrada del usuario

Recuerda que incluimos la funcionalidad de entrada/salida de la librería estándar con use std::io; en la primera línea del programa. Ahora llamaremos a la función stdin del módulo io, que nos permitirá manejar la entrada del usuario:

io::stdin()
 .read_line(&mut guess)

Si no hubiéramos importado la librería io con use std::io; al principio del programa, todavía podríamos usar la función escribiendo esta llamada a la función como std::io::stdin. La función stdin devuelve una instancia de std::io::Stdin, que es un tipo que representa un controlador de la entrada estándar de tu terminal.

A continuación, la línea .read_line(&mut guess) llama al método read_line en el controlador de entrada estándar para obtener la entrada del usuario. También estamos pasando &mut guess como argumento a read_line para decirle en qué cadena almacenar la entrada del usuario. El trabajo completo de read_line es tomar lo que el usuario escribe en la entrada estándar y anexarlo a una cadena (sin sobrescribir su contenido), por lo que pasamos esa cadena como argumento. El argumento de cadena debe ser mutable para que el método pueda cambiar el contenido de la cadena.

El & indica que este argumento es una referencia, lo que te da una forma de permitir que múltiples partes de tu código accedan a un pedazo de datos sin necesidad de copiar ese dato en la memoria múltiples veces. Las referencias son una característica compleja, y una de las principales ventajas de Rust es lo segura y fácil que es usar referencias. No necesitas saber muchos detalles de eso para terminar este programa. Por ahora, todo lo que necesitas saber es que, al igual que las variables, las referencias son inmutables por defecto. Por lo tanto, debes escribir &mut guess en lugar de &guess para que sea mutable. (El Capítulo 4 explicará las referencias más detenidamente.)

Manejando posibles errores con Result

Todavía estamos trabajando en esta línea de código. Ahora estamos discutiendo una tercera línea de texto, pero ten en cuenta que todavía es parte de una sola línea lógica de código. La siguiente parte es este método:

.expect("Failed to read line");

Podríamos haber escrito este código como:

io::stdin().read_line(&mut guess).expect("Failed to read line");

Sin embargo, una línea larga es difícil de leer, por lo que es mejor dividirla. A menudo es prudente introducir un salto de línea y otros espacios en blanco para ayudar a dividir las líneas largas cuando llamas a un método con la sintaxis .method_name(). Ahora vamos a discutir lo que hace esta línea.

Como se mencionó anteriormente, read_line coloca lo que el usuario ingresa en la cadena que le pasamos, pero también devuelve un valor de tipo Result. Result es una enumeración, a menudo llamada enum, que es un tipo que puede estar en uno de varios posibles estados. Llamamos a cada estado posible una variante.

El Capítulo 6 cubrirá los enums con más detalle. El propósito de estos tipos Result es codificar información de manejo de errores.

Las variantes de Result son Ok y Err. La variante Ok indica que la operación tuvo éxito, y dentro de Ok está el valor generado con éxito. La variante Err significa que la operación falló, y Err contiene información sobre cómo o por qué la operación falló.

Los valores del tipo Result, al igual que los valores de cualquier tipo, tienen métodos definidos en ellos. Una instancia de Result tiene un método expect que puedes llamar. Si esta instancia de Result es un valor Err, expect hará que el programa se detenga y muestre el mensaje que le pasaste como argumento a expect. Si el método read_line devuelve un Err, probablemente sea el resultado de un error proveniente del sistema operativo subyacente. Si esta instancia de Result es un valor Ok, expect tomará el valor de retorno que está almacenando Ok y te devolverá solo ese valor para que puedas usarlo. En este caso, ese valor es el número de bytes de la entrada del usuario.

Si no llamas a expect, el programa se compilará, pero recibirás una advertencia:

$ cargo build
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
warning: unused `Result` that must be used
  --> src/main.rs:10:5
   |
10 |     io::stdin().read_line(&mut guess);
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: `#[warn(unused_must_use)]` on by default
   = note: this `Result` may be an `Err` variant, which should be handled

warning: `guessing_game` (bin "guessing_game") generated 1 warning
    Finished dev [unoptimized + debuginfo] target(s) in 0.59s

Rust advierte que no has usado el valor Result devuelto por read_line, lo que indica que el programa no ha manejado un posible error.

La forma correcta de suprimir la advertencia es escribir realmente código de manejo de errores, pero en nuestro caso solo queremos detener este programa cuando ocurra un problema, por lo que podemos usar expect. Aprenderás sobre cómo recuperarse de errores en el Capítulo 9.

Imprimiendo valores con marcadores de posición de println!

Aparte de la llave curva de cierre, solo queda una línea más por discutir en el código hasta ahora:

println!("You guessed: {guess}");

Esta línea imprime la cadena que ahora contiene la entrada del usuario. El conjunto de llaves {} es un marcador de posición: piensa en {} como pequeños pinzas de cangrejo que mantienen un valor en su lugar. Cuando se imprime el valor de una variable, el nombre de la variable puede ir dentro de las llaves. Cuando se imprime el resultado de evaluar una expresión, coloca llaves vacías en la cadena de formato, luego sigue la cadena de formato con una lista separada por comas de expresiones para imprimir en cada marcador de posición de llaves vacías en el mismo orden. Imprimir una variable y el resultado de una expresión en una llamada a println! se vería así:

let x = 5;
let y = 10;

println!("x = {x} and y + 2 = {}", y + 2);

Este código imprimiría x = 5 and y = 12.

Probando la primera parte

Probemos la primera parte del juego de adivinanza. Ejecútalo usando cargo run:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 6.44s
     Running `target/debug/guessing_game`
Adivina el número!
Por favor, ingresa tu suposición.
6
Has adivinado: 6

En este momento, la primera parte del juego está lista: estamos recibiendo la entrada desde el teclado y luego la estamos imprimiendo.

Generando un número secreto

A continuación, necesitamos generar un número secreto que el usuario intentará adivinar. El número secreto debe ser diferente cada vez para que el juego sea divertido de jugar más de una vez. Usaremos un número aleatorio entre 1 y 100 para que el juego no sea demasiado difícil. Rust todavía no incluye funcionalidad de números aleatorios en su librería estándar. Sin embargo, el equipo de Rust proporciona un crado rand en https://crates.io/crates/rand con dicha funcionalidad.

Usando un crado para obtener más funcionalidad

Recuerda que un crado es una colección de archivos de código fuente de Rust. El proyecto que hemos estado construyendo es un crado binario, que es un ejecutable. El crado rand es un crado de biblioteca, que contiene código que está destinado a ser utilizado en otros programas y no puede ejecutarse por sí solo.

La coordinación de crados externos de Cargo es donde Cargo realmente se destaca. Antes de que podamos escribir código que use rand, necesitamos modificar el archivo Cargo.toml para incluir el crado rand como una dependencia. Abre ese archivo ahora y agrega la siguiente línea al final, debajo de la cabecera de la sección [dependencies] que Cargo creó para ti. Asegúrate de especificar rand exactamente como lo tenemos aquí, con este número de versión, o los ejemplos de código de este tutorial es posible que no funcionen:

Nombre del archivo: Cargo.toml

[dependencies]
rand = "0.8.5"

En el archivo Cargo.toml, todo lo que sigue a una cabecera es parte de esa sección que continúa hasta que comienza otra sección. En [dependencies] le dices a Cargo cuales son los crados externos en los que depende tu proyecto y cuales versiones de esos crados requieres. En este caso, especificamos el crado rand con el especificador de versión semántica 0.8.5. Cargo entiende la Versionamiento Semántico (a veces llamado SemVer), que es un estándar para escribir números de versión. El especificador 0.8.5 es en realidad un atajo para ^0.8.5, lo que significa cualquier versión que sea al menos 0.8.5 pero menor que 0.9.0.

Cargo considera que estas versiones tienen una API pública compatible con la versión 0.8.5, y esta especificación asegura que obtendrás la última versión de parche que todavía se compilará con el código de este capítulo. No se garantiza que cualquier versión 0.9.0 o mayor tenga la misma API que la que usan los siguientes ejemplos.

Ahora, sin cambiar ningún código, construyamos el proyecto, como se muestra en la Lista 2-2.

$ cargo build
    Updating crates.io index
  Downloaded rand v0.8.5
  Downloaded libc v0.2.127
  Downloaded getrandom v0.2.7
  Downloaded cfg-if v1.0.0
  Downloaded ppv-lite86 v0.2.16
  Downloaded rand_chacha v0.3.1
  Downloaded rand_core v0.6.3
   Compiling rand_core v0.6.3
   Compiling libc v0.2.127
   Compiling getrandom v0.2.7
   Compiling cfg-if v1.0.0
   Compiling ppv-lite86 v0.2.16
   Compiling rand_chacha v0.3.1
   Compiling rand v0.8.5
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 2.53s

Lista 2-2: La salida de ejecutar cargo build después de agregar el crado rand como una dependencia

Es posible que veas números de versión diferentes (pero todos serán compatibles con el código, gracias a SemVer!) y líneas diferentes (dependiendo del sistema operativo), y las líneas pueden estar en un orden diferente.

Cuando incluimos una dependencia externa, Cargo obtiene las últimas versiones de todo lo que necesita esa dependencia desde el registro, que es una copia de los datos de Crates.io en https://crates.io. Crates.io es donde las personas en el ecosistema de Rust publican sus proyectos de Rust de código abierto para que otros los usen.

Después de actualizar el registro, Cargo revisa la sección [dependencies] y descarga cualquier crado que se liste y que no haya sido descargado ya. En este caso, aunque solo listamos rand como una dependencia, Cargo también capturó otros crates en los que rand depende para funcionar. Después de descargar los crates, Rust los compila y luego compila el proyecto con las dependencias disponibles.

Si ejecutas inmediatamente cargo build nuevamente sin hacer ningún cambio, no recibirás ninguna salida aparte de la línea Finished. Cargo sabe que ya ha descargado y compilado las dependencias, y no has cambiado nada sobre ellas en tu archivo Cargo.toml. Cargo también sabe que no has cambiado nada en tu código, por lo que tampoco lo recompila. Sin nada que hacer, simplemente sale.

Si abres el archivo src/main.rs, haces un cambio trivial, luego lo guardas y lo vuelves a compilar, solo verás dos líneas de salida:

$ cargo build
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 2.53 secs

Estas líneas muestran que Cargo solo actualiza la compilación con tu pequeño cambio en el archivo src/main.rs. Tus dependencias no han cambiado, por lo que Cargo sabe que puede reutilizar lo que ya ha descargado y compilado para esas.

Asegurando compilaciones reproducibles con el archivo Cargo.lock

Cargo tiene un mecanismo que te asegura que puedas reconstruir el mismo artefacto cada vez que tú o alguien más compile tu código: Cargo usará solo las versiones de las dependencias que has especificado hasta que lo indiques de otra manera. Por ejemplo, digamos que la próxima semana sale la versión 0.8.6 del crado rand, y esa versión contiene una importante corrección de errores, pero también contiene una regresión que romperá tu código. Para manejar esto, Rust crea el archivo Cargo.lock la primera vez que ejecutas cargo build, por lo que ahora tenemos esto en el directorio guessing_game.

Cuando construyes un proyecto por primera vez, Cargo determina todas las versiones de las dependencias que cumplen con los criterios y luego las escribe en el archivo Cargo.lock. Cuando construyas tu proyecto en el futuro, Cargo verá que existe el archivo Cargo.lock y usará las versiones especificadas allí en lugar de hacer todo el trabajo de determinar las versiones nuevamente. Esto te permite tener una compilación reproducible automáticamente. En otras palabras, tu proyecto permanecerá en 0.8.5 hasta que lo actualices explícitamente, gracias al archivo Cargo.lock. Debido a que el archivo Cargo.lock es importante para las compilaciones reproducibles, a menudo se incluye en el control de código fuente con el resto del código de tu proyecto.

Actualizando un crado para obtener una nueva versión

Cuando quieres actualizar un crado, Cargo proporciona el comando update, que ignorará el archivo Cargo.lock y determinará todas las últimas versiones que coinciden con tus especificaciones en Cargo.toml. Luego, Cargo escribirá esas versiones en el archivo Cargo.lock. De lo contrario, por defecto, Cargo solo buscará versiones mayores que 0.8.5 y menores que 0.9.0. Si el crado rand ha lanzado las dos nuevas versiones 0.8.6 y 0.9.0, verías lo siguiente si ejecutaras cargo update:

$ cargo update
Updating crates.io index
Updating rand v0.8.5 - > v0.8.6

Cargo ignora la versión 0.9.0. En este momento, también notarías un cambio en tu archivo Cargo.lock que indica que la versión del crado rand que estás usando ahora es 0.8.6. Para usar la versión 0.9.0 de rand o cualquier versión en la serie 0.9._x_, tendrías que actualizar el archivo Cargo.toml para que se vea así en su lugar:

[dependencies]
rand = "0.9.0"

La próxima vez que ejecutes cargo build, Cargo actualizará el registro de crates disponibles y reevaluará tus requisitos de rand de acuerdo con la nueva versión que has especificado.

Hay mucho más que decir sobre Cargo y su ecosistema, lo que discutiremos en el Capítulo 14, pero por ahora, eso es todo lo que necesitas saber. Cargo hace muy fácil reutilizar bibliotecas, por lo que los rustaceos pueden escribir proyectos más pequeños que se componen de una serie de paquetes.

Generando un número aleatorio

Comencemos a usar rand para generar un número que se tenga que adivinar. El siguiente paso es actualizar src/main.rs, como se muestra en la Lista 2-3.

Nombre del archivo: src/main.rs

use std::io;
1 use rand::Rng;

fn main() {
    println!("Adivina el número!");

  2 let secret_number = rand::thread_rng().gen_range(1..=100);

  3 println!("El número secreto es: {secret_number}");

    println!("Por favor, ingresa tu suposición.");

    let mut guess = String::new();

    io::stdin()
     .read_line(&mut guess)
     .expect("Falló al leer la línea");

    println!("Has adivinado: {guess}");
}

Lista 2-3: Agregando código para generar un número aleatorio

Primero agregamos la línea use rand::Rng; [1]. El trato Rng define métodos que los generadores de números aleatorios implementan, y este trato debe estar en ámbito para que podamos usar esos métodos. El Capítulo 10 cubrirá los tratados en detalle.

Luego, estamos agregando dos líneas en el medio. En la primera línea [2], llamamos a la función rand::thread_rng que nos da el generador de números aleatorios particular que vamos a usar: uno que es local al hilo de ejecución actual y está sembrado por el sistema operativo. Luego llamamos al método gen_range en el generador de números aleatorios. Este método está definido por el trato Rng que trajimos al ámbito con la declaración use rand::Rng;. El método gen_range toma una expresión de rango como argumento y genera un número aleatorio en el rango. El tipo de expresión de rango que estamos usando aquí tiene la forma start..=end y es inclusiva en los límites inferior y superior, por lo que necesitamos especificar 1..=100 para solicitar un número entre 1 y 100.

Nota: No sabrás solo qué tratados usar y qué métodos y funciones llamar de un crado, por lo que cada crado tiene documentación con instrucciones para usarlo. Otra característica genial de Cargo es que ejecutar el comando cargo doc --open construirá la documentación proporcionada por todas tus dependencias localmente y la abrirá en tu navegador. Si estás interesado en otra funcionalidad en el crado rand, por ejemplo, ejecuta cargo doc --open y haz clic en rand en la barra lateral izquierda.

La segunda línea nueva [3] imprime el número secreto. Esto es útil mientras desarrollamos el programa para poder probarlo, pero lo eliminaremos de la versión final. No es mucho de un juego si el programa imprime la respuesta tan pronto como comienza!

Intenta ejecutar el programa varias veces:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 2.53s
     Running `target/debug/guessing_game`
Adivina el número!
El número secreto es: 7
Por favor, ingresa tu suposición.
4
Has adivinado: 4

$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.02s
     Running `target/debug/guessing_game`
Adivina el número!
El número secreto es: 83
Por favor, ingresa tu suposición.
5
Has adivinado: 5

Deberías obtener números aleatorios diferentes, y todos deberían ser números entre 1 y 100. ¡Excelente trabajo!

Comparando la suposición con el número secreto

Ahora que tenemos la entrada del usuario y un número aleatorio, podemos compararlos. Ese paso se muestra en la Lista 2-4. Tenga en cuenta que este código todavía no se compilará, como se explicará.

Nombre del archivo: src/main.rs

use rand::Rng;
1 use std::cmp::Ordering;
use std::io;

fn main() {
    --snip--

    println!("Has adivinado: {guess}");

  2 match guess.3 cmp(&secret_number) {
        Ordering::Less => println!("Demasiado pequeño!"),
        Ordering::Greater => println!("Demasiado grande!"),
        Ordering::Equal => println!("¡Ganaste!"),
    }
}

Lista 2-4: Manejar los posibles valores de retorno de la comparación de dos números

Primero agregamos otra declaración use [1], trayendo un tipo llamado std::cmp::Ordering al ámbito desde la biblioteca estándar. El tipo Ordering es otro enum y tiene las variantes Less, Greater y Equal. Estos son los tres resultados posibles cuando se comparan dos valores.

Luego agregamos cinco líneas nuevas al final que usan el tipo Ordering. El método cmp [3] compara dos valores y se puede llamar en cualquier cosa que se pueda comparar. Toma una referencia a lo que quieres comparar: aquí está comparando guess con secret_number. Luego devuelve una variante del enum Ordering que trajimos al ámbito con la declaración use. Usamos una expresión match [2] para decidir qué hacer a continuación basado en qué variante de Ordering se devolvió desde la llamada a cmp con los valores en guess y secret_number.

Una expresión match está compuesta por ramas. Una rama consta de un patrón contra el que se debe coincidir, y el código que debe ejecutarse si el valor dado a match coincide con el patrón de esa rama. Rust toma el valor dado a match y lo revisa en cada patrón de rama por turnos. Los patrones y la construcción match son características poderosas de Rust: te permiten expresar una variedad de situaciones que tu código podría encontrar y te aseguran que las manejes todas. Estas características se cubrirán en detalle en el Capítulo 6 y el Capítulo 18, respectivamente.

Veamos un ejemplo con la expresión match que usamos aquí. Digamos que el usuario ha adivinado 50 y el número secreto generado aleatoriamente esta vez es 38.

Cuando el código compara 50 con 38, el método cmp devolverá Ordering::Greater porque 50 es mayor que 38. La expresión match obtiene el valor Ordering::Greater y comienza a revisar cada patrón de rama. Mira el patrón de la primera rama, Ordering::Less, y ve que el valor Ordering::Greater no coincide con Ordering::Less, por lo que ignora el código en esa rama y pasa a la siguiente rama. El patrón de la siguiente rama es Ordering::Greater, que coincide con Ordering::Greater ¡El código asociado a esa rama se ejecutará y imprimirá Demasiado grande! en la pantalla. La expresión match termina después de la primera coincidencia exitosa, por lo que no revisará la última rama en este escenario.

Sin embargo, el código en la Lista 2-4 todavía no se compilará. Intentemoslo:

$ cargo build
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
error[E0308]: tipos no coincidentes
  --> src/main.rs:22:21
   |
22 |     match guess.cmp(&secret_number) {
   |                     ^^^^^^^^^^^^^^ se esperaba struct `String`, encontrado integer
   |
   = nota: se esperaba referencia `&String`
              se encontró referencia `&{integer}`

El núcleo del error indica que hay tipos no coincidentes. Rust tiene un sistema de tipos estático fuerte. Sin embargo, también tiene inferencia de tipos. Cuando escribimos let mut guess = String::new(), Rust pudo inferir que guess debería ser un String y no nos hizo escribir el tipo. Por otro lado, secret_number es un tipo numérico. Algunos de los tipos numéricos de Rust pueden tener un valor entre 1 y 100: i32, un número de 32 bits; u32, un número sin signo de 32 bits; i64, un número de 64 bits; así como otros. A menos que se especifique lo contrario, Rust por defecto es un i32, que es el tipo de secret_number a menos que agregues información de tipo en otro lugar que haga que Rust infiera un tipo numérico diferente. La razón del error es que Rust no puede comparar una cadena y un tipo numérico.

En última instancia, queremos convertir la String que el programa lee como entrada en un tipo numérico real para que podamos compararla numéricamente con el número secreto. Lo hacemos agregando esta línea al cuerpo de la función main:

Nombre del archivo: src/main.rs

use std::io;
use rand::Rng;
use std::cmp::Ordering;

fn main() {
    println!("Adivina el número!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    println!("El número secreto es: {secret_number}");

    println!("Por favor, ingresa tu suposición.");

    let mut guess = String::new();

    io::stdin()
      .read_line(&mut guess)
      .expect("Falló al leer la línea");

    let guess: u32 = guess
      .trim()
      .parse()
      .expect("Por favor, escribe un número!");

    println!("Has adivinado: {guess}");

    match guess.cmp(&secret_number) {
        Ordering::Less => println!("Demasiado pequeño!"),
        Ordering::Greater => println!("Demasiado grande!"),
        Ordering::Equal => println!("¡Ganaste!"),
    }
}

Creamos una variable llamada guess. Pero espera, ¿no tiene el programa ya una variable llamada guess? Lo tiene, pero afortunadamente Rust permite que sobrescribamos el valor anterior de guess con uno nuevo. El sobreescritura nos permite reutilizar el nombre de variable guess en lugar de obligarnos a crear dos variables únicas, como guess_str y guess, por ejemplo. Cubriremos esto con más detalle en el Capítulo 3, pero por ahora, sabe que esta característica se usa a menudo cuando quieres convertir un valor de un tipo a otro tipo.

Asociamos esta nueva variable a la expresión guess.trim().parse(). La guess en la expresión se refiere a la variable original guess que contenía la entrada como una cadena. El método trim en una instancia de String eliminará cualquier espacio en blanco al principio y al final, lo que debemos hacer para poder comparar la cadena con el u32, que solo puede contener datos numéricos. El usuario debe presionar enter para satisfacer read_line e ingresar su suposición, lo que agrega un carácter de nueva línea a la cadena. Por ejemplo, si el usuario escribe 5 y presiona enter, guess se ve así: 5\n. El \n representa "nueva línea". (En Windows, presionar enter resulta en un retorno de carro y una nueva línea, \r\n.) El método trim elimina \n o \r\n, resultando en solo 5.

El método parse en cadenas convierte una cadena a otro tipo. Aquí, lo usamos para convertir de una cadena a un número. Necesitamos decirle a Rust el tipo numérico exacto que queremos usando let guess: u32. El dos puntos (:) después de guess le dice a Rust que anotaremos el tipo de la variable. Rust tiene algunos tipos numéricos integrados; el u32 que se ve aquí es un entero sin signo de 32 bits. Es una buena opción predeterminada para un número positivo pequeño. Aprenderás sobre otros tipos numéricos en el Capítulo 3.

Además, la anotación u32 en este programa de ejemplo y la comparación con secret_number significa que Rust inferirá que secret_number también debería ser un u32. Entonces, ahora la comparación será entre dos valores del mismo tipo ¡

El método parse solo funcionará en caracteres que se pueden convertir lógicamente en números y, por lo tanto, puede causar fácilmente errores. Si, por ejemplo, la cadena contuviera A👍%, no habría forma de convertir eso en un número. Debido a que podría fallar, el método parse devuelve un tipo Result, al igual que el método read_line (discutido anteriormente en "Manejar el fracaso potencial con Result"). Vamos a tratar este Result de la misma manera usando el método expect nuevamente. Si parse devuelve una variante Err de Result porque no pudo crear un número a partir de la cadena, la llamada a expect detendrá el juego y imprimirá el mensaje que le demos. Si parse puede convertir correctamente la cadena a un número, devolverá la variante Ok de Result, y expect devolverá el número que queremos del valor Ok.

Ahora ejecutemos el programa:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 0.43s
     Running `target/debug/guessing_game`
Adivina el número!
El número secreto es: 58
Por favor, ingresa tu suposición.
  76
Has adivinado: 76
Demasiado grande!

¡Genial! Aunque se agregaron espacios antes de la suposición, el programa todavía pudo determinar que el usuario adivinó 76. Ejecute el programa varias veces para verificar el comportamiento diferente con diferentes tipos de entrada: adivine el número correctamente, adivine un número que sea demasiado alto y adivine un número que sea demasiado bajo.

Ya tenemos la mayor parte del juego funcionando, pero el usuario solo puede hacer una suposición. Cambiemos eso agregando un bucle ¡

Permitiendo múltiples suposiciones con bucles

La palabra clave loop crea un bucle infinito. Agregaremos un bucle para dar a los usuarios más oportunidades de adivinar el número:

Nombre del archivo: src/main.rs

use std::io;
use rand::Rng;
use std::cmp::Ordering;

fn main() {
    println!("Adivina el número!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    println!("El número secreto es: {secret_number}");

    loop {
        println!("Por favor, ingresa tu suposición.");
        let mut guess = String::new();

        io::stdin()
         .read_line(&mut guess)
         .expect("Falló al leer la línea");

        let guess: u32 = guess
         .trim()
         .parse()
         .expect("Por favor, escribe un número!");

        println!("Has adivinado: {guess}");

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Demasiado pequeño!"),
            Ordering::Greater => println!("Demasiado grande!"),
            Ordering::Equal => println!("¡Ganaste!"),
        }
    }
}

Como puede ver, hemos movido todo desde el prompt de entrada de la suposición en adelante dentro de un bucle. Asegúrese de indentar las líneas dentro del bucle con otras cuatro espacios cada una y ejecutar el programa nuevamente. El programa ahora pedirá otra suposición para siempre, lo que en realidad introduce un nuevo problema. Parece que el usuario no puede salir ¡

El usuario siempre podría interrumpir el programa usando el atajo de teclado ctrl-C. Pero hay otra forma de escapar de este monstruo insaciable, como se mencionó en la discusión de parse en "Comparando la suposición con el número secreto": si el usuario ingresa una respuesta no numérica, el programa se detendrá. Podemos aprovechar eso para permitir que el usuario salga, como se muestra aquí:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 1.50s
     Running `target/debug/guessing_game`
Adivina el número!
El número secreto es: 59
Por favor, ingresa tu suposición.
45
Has adivinado: 45
Demasiado pequeño!
Por favor, ingresa tu suposición.
60
Has adivinado: 60
Demasiado grande!
Por favor, ingresa tu suposición.
59
Has adivinado: 59
¡Ganaste!
Por favor, ingresa tu suposición.
quit
thread 'main' panicked at 'Please type a number!: ParseIntError
{ kind: InvalidDigit }', src/main.rs:28:47
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Escribir quit cerrará el juego, pero como notará, también lo hará ingresando cualquier otra entrada no numérica. Esto es subóptimo, por decir lo menos; queremos que el juego también se detenga cuando se adivina el número correcto.

Saliendo después de una suposición correcta

Programemos el juego para que salga cuando el usuario gane, agregando una declaración break:

Nombre del archivo: src/main.rs

--snip--

match guess.cmp(&secret_number) {
    Ordering::Less => println!("Demasiado pequeño!"),
    Ordering::Greater => println!("Demasiado grande!"),
    Ordering::Equal => {
        println!("¡Ganaste!");
        break;
    }
}

Agregar la línea break después de ¡Ganaste! hace que el programa salga del bucle cuando el usuario adivina correctamente el número secreto. Salir del bucle también significa salir del programa, porque el bucle es la última parte de main.

Manejo de entrada no válida

Para mejorar aún más el comportamiento del juego, en lugar de detener el programa cuando el usuario ingresa un valor no numérico, hagamos que el juego ignore ese valor no numérico para que el usuario pueda seguir adivinando. Podemos hacer eso modificando la línea donde guess se convierte de una String a un u32, como se muestra en la Lista 2-5.

Nombre del archivo: src/main.rs

--snip--

io::stdin()
 .read_line(&mut guess)
 .expect("Falló al leer la línea");

let guess: u32 = match guess.trim().parse() {
    Ok(num) => num,
    Err(_) => continue,
};

println!("Has adivinado: {guess}");

--snip--

Lista 2-5: Ignorar una suposición no numérica y pedir otra suposición en lugar de detener el programa

Cambiamos de una llamada a expect a una expresión match para pasar de detener el programa en caso de error a manejar el error. Recuerde que parse devuelve un tipo Result y Result es un enum que tiene las variantes Ok y Err. Estamos usando una expresión match aquí, como lo hicimos con el resultado Ordering del método cmp.

Si parse puede convertir correctamente la cadena en un número, devolverá un valor Ok que contiene el número resultante. Ese valor Ok coincidirá con el patrón del primer brazo, y la expresión match simplemente devolverá el valor num que parse produjo y puso dentro del valor Ok. Ese número terminará exactamente donde queremos en la nueva variable guess que estamos creando.

Si parse no puede convertir la cadena en un número, devolverá un valor Err que contiene más información sobre el error. El valor Err no coincide con el patrón Ok(num) en el primer brazo de match, pero coincide con el patrón Err(_) en el segundo brazo. El guión bajo, _, es un valor genérico; en este ejemplo, estamos diciendo que queremos coincidir con todos los valores Err, sin importar qué información tengan dentro. Entonces el programa ejecutará el código del segundo brazo, continue, que le dice al programa que vaya a la siguiente iteración del loop y pida otra suposición. Entonces, en efecto, el programa ignora todos los errores que parse podría encontrar ¡

Ahora todo en el programa debería funcionar como se espera. Intentemoslo:

$ cargo run
   Compiling guessing_game v0.1.0 (file:///projects/guessing_game)
    Finished dev [unoptimized + debuginfo] target(s) in 4.45s
     Running `target/debug/guessing_game`
Adivina el número!
El número secreto es: 61
Por favor, ingresa tu suposición.
10
Has adivinado: 10
Demasiado pequeño!
Por favor, ingresa tu suposición.
99
Has adivinado: 99
Demasiado grande!
Por favor, ingresa tu suposición.
foo
Por favor, ingresa tu suposición.
61
Has adivinado: 61
¡Ganaste!

¡Genial! Con un pequeño ajuste final, terminaremos el juego de adivinanza. Recuerde que el programa todavía está imprimiendo el número secreto. Eso funcionó bien para probar, pero arruina el juego. Vamos a eliminar la println! que imprime el número secreto. La Lista 2-6 muestra el código final.

Nombre del archivo: src/main.rs

use rand::Rng;
use std::cmp::Ordering;
use std::io;

fn main() {
    println!("Adivina el número!");

    let secret_number = rand::thread_rng().gen_range(1..=100);

    loop {
        println!("Por favor, ingresa tu suposición.");

        let mut guess = String::new();

        io::stdin()
         .read_line(&mut guess)
         .expect("Falló al leer la línea");

        let guess: u32 = match guess.trim().parse() {
            Ok(num) => num,
            Err(_) => continue,
        };

        println!("Has adivinado: {guess}");

        match guess.cmp(&secret_number) {
            Ordering::Less => println!("Demasiado pequeño!"),
            Ordering::Greater => println!("Demasiado grande!"),
            Ordering::Equal => {
                println!("¡Ganaste!");
                break;
            }
        }
    }
}

Lista 2-6: Código completo del juego de adivinanza

En este punto, ha construido con éxito el juego de adivinanza. ¡Felicitaciones!

Resumen

¡Felicitaciones! Has completado el laboratorio de programación de un juego de adivinanza. Puedes practicar más laboratorios en LabEx para mejorar tus habilidades.