Rutas
/
Rust
Rust
/
Ejercicios
/
Juego de rol
Juego de rol

Juego de rol

Ejercicio de aprendizaje

Introducción

Referencias nulas

Si alguna vez has usado otro lenguaje de programación (C/C++, Python, Java, Ruby, Lisp, etc.), es probable que ya te hayas encontrado con null o nil. El uso de null o nil es la forma en que estos lenguajes indican que una variable concreta no tiene valor. Sin embargo, esto hace que usar por accidente una variable que apunta a null sea un error fácil (y frecuente) de cometer. Como puedes imaginar, intentar llamar a una función que no está ahí, o acceder a un valor que no existe, puede provocar todo tipo de bugs y fallos. El creador de null llegó a llamarlo su «error de mil millones de dólares».

El tipo Option

Para evitar estos problemas, Rust no usa referencias nulas. Sin embargo, sigue necesitando una forma segura de indicar que una variable concreta no tiene valor. Aquí es donde entra en juego Option. En lugar de tener una variable a la que le falta un valor, las variables de Rust pueden usar la enumeración Option. Esta enumeración tiene dos variantes: None, el equivalente de Rust a null; y Some(T), donde T es un valor de cualquier tipo.

Tiene este aspecto:

enum Option<T> {
    None,
    Some(T),
}

Puedes pensar en Option como una capa de seguridad entre tú y los problemas que pueden causar las referencias nulas, sin perder por ello su utilidad conceptual.

Cómo usar Option

Asignar None a una variable es bastante sencillo:

let nothing: Option<u32> = None;  // Variable nothing is set to None

Sin embargo, si quieres que el tipo Option contenga un valor, no puedes asignar ese valor directamente. Una variable de tipo Option y, por ejemplo, una variable de tipo i32 no son equivalentes. Tendrás que usar Some:

let wrong_way: Option<i32> = -4; // This will not work

let right_way: Option<i32> = Some(-4); // This will work
let another_right_way = Some(-4); // Compiler infers that this is Option<i32>

También por este motivo no funcionará lo siguiente:

let number = 47;
let option_number = Some(15);

let compile_error = number + option_number; // Cannot add an i32 and an Option<i32> - they are of different types

Si quieres obtener el valor que contiene Some, primero tendrás que comprobar que existe:

let mut some_words = Some("choose something to say"); // some_words set to something

match some_words {
    Some(str) => println!("Here, we will {}", str),
    None => println!("I've got nothing to say"),
} // Prints "Here, we will choose something to say"

some_words = None; // some_words now set to None

// exactly the same match block as above
match some_words {
    Some(str) => println!("Here, we will {}", str),
    None => println!("I've got nothing to say"),
} // Prints "I've got nothing to say"

Además de match, Rust tiene otras herramientas para comprobar y acceder a los valores que contiene Option, pero a estas alturas match ya debería resultarte familiar.

Además, considera esto una demostración de por qué Rust usa Option en lugar de una referencia nula. La cuestión es que tienes que comprobar si la variable Option es Some (en cuyo caso puedes extraer y usar el valor que contiene) o None. En cualquier otro caso, tu programa no compilará; el compilador te protege de null.

Instrucciones

Estás trabajando en la implementación de un juego de rol. El personaje del jugador se representa con lo siguiente:

pub struct Player {
    health: u32,
    mana: Option<u32>,
    level: u32,
}

En este juego, los jugadores deben alcanzar el nivel 10 antes de desbloquear una reserva de maná y poder empezar a lanzar hechizos. Antes de ese momento, el maná del jugador es None.

Estás trabajando en dos funcionalidades de este juego: la mecánica de revivir y la mecánica de lanzar hechizos.

El método revive debe comprobar que el jugador está realmente muerto (su salud ha llegado a 0) y, si es así, debe devolver una nueva instancia de Player con 100 de salud. Si el nivel del jugador es 10 o superior, también debe revivir con 100 de maná. Si el nivel del jugador es inferior a 10, su maná debe ser None. El método revive debe conservar el nivel del jugador.

let dead_player = Player { health: 0, mana: None, level: 2 };
dead_player.revive()
// Returns Player { health: 100, mana: None, level: 2 }

Si se llama al método revive sobre un jugador cuya salud es 1 o superior, el método debe devolver None.

let alive_player = Player { health: 1, mana: Some(15), level: 11 };
alive_player.revive()
// Returns None

El método cast_spell recibe una referencia mutable al jugador, además de un parámetro mana_cost que indica cuánto maná cuesta el hechizo. Devuelve la cantidad de daño que inflige el hechizo lanzado, que siempre será el doble del coste de maná del hechizo si este se lanza correctamente.

  • Si el jugador no tiene acceso a una reserva de maná, intentar lanzar el hechizo debe reducir su salud en el coste de maná del hechizo. El daño devuelto debe ser 0.

    let not_a_wizard_yet = Player { health: 79, mana: None, level: 9 };
    assert_eq!(not_a_wizard_yet.cast_spell(5), 0)
    assert_eq!(not_a_wizard_yet.health, 74);
    assert_eq!(not_a_wizard_yet.mana, None);
    
  • Si el jugador tiene una reserva de maná pero maná insuficiente, el método no debe afectar a la reserva, sino devolver 0

    let low_mana_wizard = Player { health: 93, mana: Some(3), level: 12 };
    assert_eq!(low_mana_wizard.cast_spell(10), 0);
    assert_eq!(low_mana_wizard.health, 93);
    assert_eq!(low_mana_wizard.mana, Some(3));
    
  • En caso contrario, el mana_cost debe restarse de la reserva de maná del jugador y debe devolverse la cantidad de daño correspondiente.

    let wizard = Player { health: 123, mana: Some(30), level: 18 };
    assert_eq!(wizard.cast_spell(10), 20);
    assert_eq!(wizard.health, 123);
    assert_eq!(wizard.mana, Some(20));
    

¡Diviértete!

Editar en GitHub El enlace se abre en una ventana o pestaña nueva
Rust Exercism

¿Listo para empezar Juego de rol?

Regístrate en Exercism para aprender y dominar Rust con 99 ejercicios y mentoría humana real, todo gratis.