Rust es un lenguaje con una curva de dificultad importante cuando se trata de manipular tipos de datos. Puede ser un dolor de cabeza, así que veamos algunos consejos extras.
Accediendo al índice de un vector
Cuando recorras un array con un for, a veces es interesante saber en qué índice de iteración estamos. Con un simple modificación, puedes obtener este dato en cada iteración.
Con .iter().enumerate() obtienes el index como primer valor de un ciclo for, y el valor en cuestión en el segundo parámetro. Así, ya puedes aplicar una lógica determinada si el índice es par o impar, por ejemplo.
Option, Some y None
Estas tres palabras reservadas se utilizan muchísimo en Rust y tienes que comprender su funcionamiento para sacarle el máximo provecho y entender qué está sucediendo en tu código.
Recordemos por un momento que Option<T> puede devolver un valor determinado como Some, o puede devolver un tipo de dato None, pero que este es distinto de null. En Rust, el valor nulo simplemente no existe.
En este ejemplo, la función dividir_numeros() evaluará el denominador y, si este es cero, retornara un None, de lo contrario, devolverá el resultado de la división y puedes validar este directamente en el if let Some(result) ... para crear la variable si la función devolvió un Some y luego mostrar la misma.
Otra forma de validar un Option ya sea por Some o None es con la palabra reservada match:
fndividir_numeros(numerador:i128, denominador:i128)->Option<i128>{if denominador ==0{None}else{Some(numerador / denominador)}}fnmain(){let result =dividir_numeros(10,2);match result {Some(value)=>{println!("El resultado es: {}", value)},None=>{println!("No puedes dividir por cero");},}}
match evaluará el resultado de la función por Some o por None, ejecutando el código según corresponda.
Estos conceptos realmente requieren de tiempo y paciencia para comprender. Si vienes de lenguajes como C++, tal vez te sientas algo familiarizado con la utilización de los caracteres & o de un *, pasajes por valor o referencia. Apóyate siempre de la documentación oficial de Rust para comprender cómo se utiliza cada característica y por qué.
Rust es un lenguaje con una curva de dificultad importante cuando se trata de manipular tipos de datos. Puede ser un dolor de cabeza, así que veamos algunos consejos extras.
Accediendo al índice de un vector
Cuando recorras un array con un for, a veces es interesante saber en qué índice de iteración estamos. Con un simple modificación, puedes obtener este dato en cada iteración.
fn main() {
let mut my_arr: Vec<String> = Vec::new();
my_arr.push("Primer valor".to_string());
my_arr.push("Segundo valor".to_string());
my_arr.push("Tercer valor".to_string());
for (index, word) in my_arr.iter().enumerate() {
println!("{} {}", index, word);
}
}
Con .iter().enumerate() obtienes el index como primer valor de un ciclo for, y el valor en cuestión en el segundo parámetro. Así, ya puedes aplicar una lógica determinada si el índice es par o impar, por ejemplo.
Option, Some y None
Estas tres palabras reservadas se utilizan muchísimo en Rust y tienes que comprender su funcionamiento para sacarle el máximo provecho y entender qué está sucediendo en tu código.
Recordemos por un momento que Option<T> puede devolver un valor determinado como Some, o puede devolver un tipo de dato None, pero que este es distinto de null. En Rust, el valor nulo simplemente no existe.
En este ejemplo, la función dividir_numeros() evaluará el denominador y, si este es cero, retornara un None, de lo contrario, devolverá el resultado de la división y puedes validar este directamente en el if let Some(result) ... para crear la variable si la función devolvió un Some y luego mostrar la misma.
Otra forma de validar un Option ya sea por Some o None es con la palabra reservada match:
fn dividir_numeros(numerador: i128, denominador: i128) -> Option<i128> {
if denominador == 0 {
None
} else {
Some(numerador / denominador)
}
}
fn main() {
let result = dividir_numeros(10, 2);
match result {
Some(value) => { println!("El resultado es: {}", value) },
None => { println!("No puedes dividir por cero"); },
}
}
match evaluará el resultado de la función por Some o por None, ejecutando el código según corresponda.
Estos conceptos realmente requieren de tiempo y paciencia para comprender. Si vienes de lenguajes como C++, tal vez te sientas algo familiarizado con la utilización de los caracteres & o de un *, pasajes por valor o referencia. Apóyate siempre de la documentación oficial de Rust para comprender cómo se utiliza cada característica y por qué.
Me parece un curso muy bueno ya que explica varios conceptos (no solo de Rust sino de programación a más bajo nivel) de una manera didáctica y con proyectos como este que hacen sentir al estudiante los dominios básicos del lenguaje.
Sin embargo, creo que se quedó corto en muchos otros aspectos como:
No se explicaron las keywords impl y struct
Se dejaron por fuera cosas como use y mod que son las maneras en que los proyectos se organizan en Rust (a mí parecer es básico saber de un lenguaje la organización del código)
También hizo falta (por lo menos mostrar) la keyword match que es algo diferente de Rust como lenguaje y que está relacionado con tanto uso de .unwrap()
Me gustó mucho el curso, está entrenido y práctico, pero si se esperaba que el estudiante llegara a realizar un proyecto como este, pienso que como mínimo se debieron explicar mejor la mayoría de los conceptos que se iban a utilizar con anterioridad.
Investigué un poco sobre una parte del código que no entendia.
if let Some(data) = datos_historia.get_mut(&last_record)
en la evaluación de la condición estamos comprobando si la variable data es verdadera o falsa, sin embargo en este caso, ya que el método .get_mut puede devolverno un valor None (que en condicionales es equivalente a false) ya que last_record, puede no estar en nuestro diccionario.
(*data).opciones.push(dato);
en esta parte estamos "desempaquetando" el valor que se encuentra en la dirección de memoria que nos devuelve el método .get_mut. Como este método devuelve un pointer (de un objeto de tipo DatoHistoria) no podemos aplicarle los métodos o atributos propios del tipo al que pertenece. Por eso utilizamos (*data).opciones.push(tag_actual).
En C, está sintaxis es parecida o se puede aplicar: data->opciones.push(tag_actual)
Otra forma en la que se puede escribir esta seccion es asi:
let data : Option<&mut DatoHistoria> = datos_historia.get_mut(&last_record);
match data {
Some(data) => data.opciones.push(dato),
None => {continue;},
}
En este caso ya no necesitamos "desempaquetar" el valor de pointer porque le pedimos una referencia al metodo, en lugar de un pointer.
Muy denso para alguien como yo que viene de aprender Python, pero muy emocionante a la vez. Habrá que reforzar con lecturas externas para entender mejor 💪🏽
Quiero aportar mi código donde hice unos cambios, como agregar íconos, hice más funcional el código, y que la vida sea de tipo vector que contiene char 🧡, para que se vea mejor. Además estuve usando enum y referencias. Si quieren darle un ojo aquí está el repo.
Ah, entonces el problema de que el tag sobrescribiera la entrada de hashmap en el codigo de la clase anterior solo fue que estaba incompleto.
Bueno, al final de esta el juego tiene un problema (ejecute tambien el propio archivo de la clase para asegurarme de que ese ahi estaba), si una entrada es invalida, el bucle vuelve a sumar o restar vida si se encuentra en esa parte.
Para arreglar esto, solo agrege un booleano is_frist_selection , se comprueba antes de sumar la vida, se pone false cuando la entrada es invalida y vuelve a true cuando es correcta.
use std::io::{self, Write};
use std::{collections::HashMap, fs};
use csv::{ReaderBuilder,StringRecord};
const PATH : &str = "history.csv";
const FRIST_TAG: &str = "INICIO";
fn get_input() ->io::Result<String>{
let mut input = String::new();
io::stdout().flush()?;
io::stdin().read_line(&mut input)?;
Ok(input)
}
#[derive(Debug, PartialEq)]
struct StoryLine {
data_type : String,
tag : String,
text : String,
life : i32,
options : Vec<StoryLine>
}
impl StoryLine {
fn new(row: StringRecord) -> StoryLine {
StoryLine {
data_type : row.get(0).unwrap().trim().to_string(),
tag: row.get(1).unwrap().trim().to_string(),
text: row.get(2).unwrap().trim().to_string(),
life: row.get(3).unwrap().trim().to_string().parse().unwrap_or(0),
options: Vec::new()
}
}
}
fn main() {
game(get_lines_form_path(PATH));
}
fn get_lines_form_path(path: &str) -> HashMap<String, StoryLine>{
// let mut no_used_lines : Vec<StoryLine> = Vec::new();
let mut lines : HashMap<String, StoryLine> = HashMap::new();
let content = fs::read_to_string(path).unwrap();
let mut rbr = ReaderBuilder::new().delimiter(b';').from_reader(content.as_bytes());
let mut last_line: String = String::default();
for line in rbr.records(){
let line = line.unwrap();
let new_line = StoryLine::new(line);
if new_line.data_type == "SITUACION" {
let tag = new_line.tag.clone();
lines.insert(tag.clone(), new_line);
last_line = tag
}else if new_line.data_type == "OPCION" {
if let Some(situacion) = lines.get_mut(&last_line){
situacion.options.push(new_line);
}
// else {no_used_lines.push(new_line);}
}
}
lines
}
fn game(lines: HashMap<String, StoryLine>){
let mut life : i32 = 100;
let mut current_tag: String = FRIST_TAG.to_string();
let mut is_just_selection: bool = true;
loop {
if life <=0 {
println!("Fin del juego"); break;
}
if let Some(line) = lines.get(¤t_tag){
if is_just_selection{life += line.life} //si una entrada es invalida, se vuelve a ejecutar esto
println!("Vida: {}",life);
println!("{}", line.text);
for (index,option) in line.options.iter().enumerate(){
println!("{{{}}}{}", index, option.text)
}
let chosen : usize = get_input().unwrap().trim().parse().unwrap_or(usize::max_value());
if let Some(chose_option) = line.options.get(chosen){
is_just_selection = true;
current_tag = chose_option.tag.clone();
} else {
println!("Entrada invalida");
is_just_selection = false
}
println!("")
} else {break;}
}
}