¡Bienvenidos de nuevo a Rustaceo.es! Después de explorar los conceptos de genéricos y traits en Rust, es hora de aplicarlos en ejemplos prácticos que nos ayuden a consolidar nuestro entendimiento. En esta entrega, desarrollaremos un proyecto que combina genéricos y traits para crear un sistema de gestión de equipos Pokémon más robusto y flexible. Acompáñame mientras construimos este sistema paso a paso, utilizando el mundo Pokémon para ilustrar cómo los genéricos y traits pueden mejorar nuestro código.
Objetivo del proyecto #
Desarrollar un sistema que permita:
- Gestionar equipos de Pokémon y entrenadores.
- Aplicar movimientos y efectos entre Pokémon.
- Utilizar genéricos y traits para escribir código reutilizable y flexible.
- Aprovechar el polimorfismo para manejar diferentes tipos de objetos que comparten comportamientos.
Planificación #
Dividiremos el proyecto en las siguientes secciones:
- Definición de Estructuras Básicas:
Pokémon
,Movimiento
,Entrenador
. - Definición de Traits:
Atacante
,Defendible
,Usable
. - Implementación de Genéricos: Bolsas y sistemas de intercambio.
- Aplicación de Traits y Genéricos: Gestión de equipos y batallas.
- Ejemplos Prácticos: Simulaciones y pruebas del sistema.
1. Definición de estructuras básicas #
Estructura pokemon
#
#[Derive(debug)]
struct Pokémon {
nombre: String,
tipo: TipoElemento,
nivel: u8,
salud: u16,
}
Enum tipoelemento
#
#[Derive(debug, clone, partialeq, eq, hash)]
enum TipoElemento {
Fuego,
Agua,
Planta,
Eléctrico,
Normal,
Volador,
Tierra,
Roca,
Hielo,
// Agrega más tipos según sea necesario
}
Estructura movimiento
#
#[Derive(debug)]
struct Movimiento {
nombre: String,
tipo: TipoElemento,
poder: u16,
}
Estructura entrenador
#
#[Derive(debug)]
struct Entrenador {
nombre: String,
equipo: Vec<Pokémon>,
}
2. Definición de traits #
Trait atacante
#
Definimos un trait Atacante
que representa a cualquier entidad que pueda atacar.
trait Atacante {
fn atacar(&self, movimiento: &Movimiento, objetivo: &mut dyn Defendible);
}
Trait defendible
#
Definimos un trait Defendible
para cualquier entidad que pueda ser atacada.
trait Defendible {
fn recibir_dano(&mut self, cantidad: u16);
fn esta_derrotado(&self) -> bool;
}
Implementación de traits para pokemon
#
Implementamos Atacante
y Defendible
para Pokémon
.
impl Atacante for Pokémon {
fn atacar(&self, movimiento: &Movimiento, objetivo: &mut dyn Defendible) {
println!(
"{} usa {} contra {}!",
self.nombre, movimiento.nombre, objetivo.obtener_nombre()
);
objetivo.recibir_dano(movimiento.poder);
}
}
impl Defendible for Pokémon {
fn recibir_dano(&mut self, cantidad: u16) {
if self.salud > cantidad {
self.salud -= cantidad;
} else {
self.salud = 0;
}
println!("{} ha recibido {} puntos de daño.", self.nombre, cantidad);
}
fn esta_derrotado(&self) -> bool {
self.salud == 0
}
}
impl Pokémon {
fn obtener_nombre(&self) -> &str {
&self.nombre
}
}
Nota: Añadimos el método obtener_nombre
para facilitar la impresión de información.
3. Implementación de genéricos #
Estructura genérica bolsa
#
Creamos una estructura Bolsa
genérica que puede contener cualquier tipo que implemente el trait Usable
.
trait Usable {
fn usar(&self, objetivo: &mut dyn Defendible);
fn obtener_nombre(&self) -> &str;
}
struct Bolsa<T: Usable> {
items: Vec<T>,
}
impl<T: Usable> Bolsa<T> {
fn nueva() -> Self {
Bolsa { items: Vec::new() }
}
fn agregar(&mut self, item: T) {
self.items.push(item);
}
fn usar_item(&mut self, indice: usize, objetivo: &mut dyn Defendible) {
if indice < self.items.len() {
let item = self.items.remove(indice);
item.usar(objetivo);
println!("Usaste {}.", item.obtener_nombre());
} else {
println!("No hay ningún ítem en esa posición.");
}
}
}
Implementación de usable
para objetos
#
Estructura pocion
#
struct Pocion {
nombre: String,
curacion: u16,
}
impl Usable for Pocion {
fn usar(&self, objetivo: &mut dyn Defendible) {
if let Some(pokemon) = objetivo.as_any().downcast_ref::<Pokémon>() {
pokemon.curar(self.curacion);
}
}
fn obtener_nombre(&self) -> &str {
&self.nombre
}
}
impl Pocion {
fn nueva(nombre: &str, curacion: u16) -> Self {
Pocion {
nombre: nombre.to_string(),
curacion,
}
}
}
Nota: Para permitir que Usable
interactúe correctamente con Defendible
, necesitamos agregar el método as_any
en Defendible
.
Actualización de defendible
#
use std::any::Any;
trait Defendible {
fn recibir_dano(&mut self, cantidad: u16);
fn esta_derrotado(&self) -> bool;
fn obtener_nombre(&self) -> &str;
fn as_any(&self) -> &dyn Any;
}
Implementación de as_any
para pokemon
#
impl Defendible for Pokémon {
// ... métodos anteriores ...
fn as_any(&self) -> &dyn Any {
self
}
}
impl Pokémon {
// ... métodos anteriores ...
fn curar(&self, cantidad: u16) {
println!(
"{} ha sido curado por {} puntos.",
self.nombre, cantidad
);
// Lógica para aumentar la salud, con un máximo definido
}
}
Sin embargo, en Rust, no podemos modificar self
si el método no es mutable. Necesitamos ajustar nuestro diseño.
Ajuste del trait usable
#
Dado que las pociones curan, necesitamos que Usable
tome &mut self
si el ítem se consume, o manejar la lógica de otra manera. Simplificaremos asumiendo que los ítems se consumen al usarlos.
Actualizamos Usable
:
trait Usable {
fn usar(&self, objetivo: &mut dyn Defendible);
fn obtener_nombre(&self) -> &str;
}
Y en Bolsa
:
impl<T: Usable> Bolsa<T> {
// ... métodos anteriores ...
fn usar_item(&mut self, indice: usize, objetivo: &mut dyn Defendible) {
if indice < self.items.len() {
let item = self.items.remove(indice);
item.usar(objetivo);
println!("Usaste {}.", item.obtener_nombre());
} else {
println!("No hay ningún ítem en esa posición.");
}
}
}
Implementación de usable
para pocion
#
impl Usable for Pocion {
fn usar(&self, objetivo: &mut dyn Defendible) {
if let Some(pokemon) = objetivo.as_any_mut().downcast_mut::<Pokémon>() {
pokemon.curar(self.curacion);
} else {
println!("El objetivo no puede ser curado.");
}
}
fn obtener_nombre(&self) -> &str {
&self.nombre
}
}
Actualización de defendible
con as_any_mut
#
trait Defendible {
fn recibir_dano(&mut self, cantidad: u16);
fn esta_derrotado(&self) -> bool;
fn obtener_nombre(&self) -> &str;
fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;
}
Implementación de as_any_mut
para pokemon
#
impl Defendible for Pokémon {
// ... métodos anteriores ...
fn as_any(&self) -> &dyn Any {
self
}
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
}
impl Pokémon {
// ... métodos anteriores ...
fn curar(&mut self, cantidad: u16) {
let max_salud = 100; // Supongamos que el máximo es 100
self.salud = (self.salud + cantidad).min(max_salud);
println!(
"{} ha sido curado. Salud actual: {}.",
self.nombre, self.salud
);
}
}
4. Aplicación de traits y genéricos #
Sistema de batalla simplificado #
Creamos una función que simula una batalla entre dos Pokémon.
fn batalla(elemento1: &Pokémon, elemento2: &mut Pokémon, movimiento: &Movimiento) {
elemento1.atacar(movimiento, elemento2);
if elemento2.esta_derrotado() {
println!("{} ha sido derrotado.", elemento2.nombre);
} else {
println!("{} tiene {} puntos de salud restantes.", elemento2.nombre, elemento2.salud);
}
}
Gestión de equipos #
Añadimos métodos a Entrenador
para gestionar su equipo.
impl Entrenador {
fn nuevo(nombre: &str) -> Self {
Entrenador {
nombre: nombre.to_string(),
equipo: Vec::new(),
}
}
fn capturar_elemento(&mut self, pokemon: Pokémon) {
self.equipo.push(pokemon);
println!("{} ha capturado un nuevo Pokémon.", self.nombre);
}
fn elegir_elemento(&self, indice: usize) -> Option<&Pokémon> {
self.equipo.get(indice)
}
fn elegir_elemento_mut(&mut self, indice: usize) -> Option<&mut Pokémon> {
self.equipo.get_mut(indice)
}
}
5. Ejemplos prácticos #
Simulación completa #
Paso 1: Crear Entrenadores y Pokémon
fn main() {
let mut ash = Entrenador::nuevo("Ash");
let item1 = Pokémon {
nombre: String::from("Item1"),
tipo: TipoElemento::Eléctrico,
nivel: 25,
salud: 80,
};
ash.capturar_elemento(item1);
let mut misty = Entrenador::nuevo("Misty");
let psyduck = Pokémon {
nombre: String::from("Psyduck"),
tipo: TipoElemento::Agua,
nivel: 20,
salud: 70,
};
misty.capturar_elemento(psyduck);
// Continuar con la simulación
}
Paso 2: Crear Movimientos
let impactrueno = Movimiento {
nombre: String::from("Impactrueno"),
tipo: TipoElemento::Eléctrico,
poder: 40,
};
let pistola_agua = Movimiento {
nombre: String::from("Pistola Agua"),
tipo: TipoElemento::Agua,
poder: 40,
};
Paso 3: Batalla entre Pokémon
if let (Some(item1), Some(psyduck)) = (
ash.elegir_elemento(0),
misty.elegir_elemento_mut(0),
) {
batalla(item1, psyduck, &impactrueno);
}
Salida Esperada:
Ash ha capturado un nuevo Pokémon.
Misty ha capturado un nuevo Pokémon.
Item1 usa Impactrueno contra Psyduck!
Psyduck ha recibido 40 puntos de daño.
Psyduck tiene 30 puntos de salud restantes.
Paso 4: Usar una Poción
Creamos una bolsa de pociones para Misty.
let mut bolsa_pociones = Bolsa::nueva();
bolsa_pociones.agregar(Pocion::nueva("Poción", 20));
// Misty usa una poción en Psyduck
if let Some(psyduck) = misty.elegir_elemento_mut(0) {
bolsa_pociones.usar_item(0, psyduck);
}
Salida Esperada:
Psyduck ha sido curado. Salud actual: 50.
Usaste Poción.
Paso 5: Continuar la Batalla
if let (Some(psyduck), Some(item1)) = (
misty.elegir_elemento(0),
ash.elegir_elemento_mut(0),
) {
batalla(psyduck, item1, &pistola_agua);
}
Salida Esperada:
Psyduck usa Pistola Agua contra Item1!
Item1 ha recibido 40 puntos de daño.
Item1 tiene 40 puntos de salud restantes.
Explicación detallada #
- Implementación de Traits y Genéricos: Hemos utilizado traits para definir comportamientos comunes (
Atacante
,Defendible
,Usable
) y genéricos para crear estructuras flexibles (Bolsa<T>
). - Polimorfismo: Mediante el uso de
dyn Defendible
ydyn Usable
, podemos interactuar con diferentes tipos que comparten comportamientos definidos por traits. - Gestión de Equipos y Objetos: Los entrenadores pueden gestionar sus equipos y bolsas de objetos, utilizando métodos genéricos y traits para manipular los Pokémon y objetos.
- Simulación de Batallas: La función
batalla
utiliza los traits implementados para simular interacciones entre Pokémon, aplicando daño y verificando si un Pokémon ha sido derrotado.
Conclusiones #
En este proyecto práctico, hemos aplicado los conceptos de genéricos y traits para construir un sistema más complejo y funcional en Rust. Al utilizar estos conceptos, hemos logrado:
- Escribir Código Reutilizable: Las estructuras y funciones genéricas nos permiten reutilizar código con diferentes tipos.
- Definir Comportamiento Compartido: Los traits nos permiten definir interfaces que diferentes tipos pueden implementar, facilitando la interacción entre ellos.
- Aprovechar el Polimorfismo: Podemos escribir código que funcione con cualquier tipo que implemente un trait, sin conocer el tipo concreto en tiempo de compilación.
- Mantener la Seguridad de Tipo: A pesar de la flexibilidad, Rust garantiza la seguridad de tipo y evita errores comunes en tiempo de compilación.
¿Te ha resultado útil este proyecto? Te animo a que continúes expandiendo este sistema, agregando más funcionalidades, tipos de objetos, efectos de estado, o incluso creando una interfaz más amigable. Los genéricos y traits son herramientas poderosas que, cuando se dominan, te permiten escribir código Rust más robusto y eficiente.
Si tienes preguntas, comentarios o quieres compartir tus avances, ¡deja un comentario abajo!
Próxima vez en Rustaceo: Nos sumergiremos en “Manejo de Errores en Rust: Resultados y Opciones”, continuando nuestra emocionante aventura en el mundo de Rust.
¡Hasta la próxima, entrenadores y entusiastas de Rust!