diff --git a/Cargo.toml b/Cargo.toml index f656a09..403b5d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,3 +11,4 @@ rand_pcg = { version = "0.3.1", features = ["serde1"] } serde = { version = "1.0.197", features = ["derive", "rc"] } typetag = "0.2.16" dyn-clone = "1.0.17" +console = "0.15.8" diff --git a/src/es03_game/cell.rs b/src/es03_game/cell.rs index 334932a..619b016 100644 --- a/src/es03_game/cell.rs +++ b/src/es03_game/cell.rs @@ -2,6 +2,7 @@ use super::entities::{Action, Direction, Entity}; use dyn_clone::{clone_trait_object, DynClone}; use rand::Rng; use serde::{Deserialize, Serialize}; +use std::fmt::Display; /// Rappresentazione di una cella di spazio.\ /// Essa ha diversi valori in base a cosa si può fare o meno su di essa. @@ -38,6 +39,23 @@ impl Cell { _ => (), } } + /// Restituisce la rappresentazione della cella in formato char, in questo modo + /// può essere utilizzata per vedere il valore e mostrarlo a terminale. + pub fn as_char(&self) -> char { + match self { + Cell::Entance => ' ', + Cell::Exit => '¤', + Cell::Special(_) => '§', + Cell::Wall => '█', + Cell::Empty => ' ', + } + } +} + +impl Display for Cell { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_char()) + } } /// Trait che permette di implementare un effetto speciale di una diff --git a/src/es03_game/config.rs b/src/es03_game/config.rs index ca6290b..d79b7c7 100644 --- a/src/es03_game/config.rs +++ b/src/es03_game/config.rs @@ -1,6 +1,6 @@ use super::{ cell::{Confusion, Effect, InstantDamage}, - entities::Decider, + entities::Behavior, }; use serde::{Deserialize, Serialize}; use std::ops::Range; @@ -52,7 +52,7 @@ pub struct ConfigPlayer { pub struct ConfigEntity { pub floors: Range, pub name: String, - pub decider: Box, + pub decider: Box, pub health: i32, pub attack: i32, pub priority: usize, diff --git a/src/es03_game/entities.rs b/src/es03_game/entities.rs index 9e2384d..c2d388e 100644 --- a/src/es03_game/entities.rs +++ b/src/es03_game/entities.rs @@ -1,20 +1,23 @@ -use super::{cell::Effect, floor::FloorPtr}; +use super::{ + cell::Effect, + floor::{FloorPtr, FloorView}, +}; use dyn_clone::{clone_trait_object, DynClone}; use rand::Rng; use rand_pcg::Pcg32; use serde::{Deserialize, Serialize}; -use std::{collections::VecDeque, fmt::Display, mem}; +use std::{collections::VecDeque, fmt::Display, io::Write, mem}; /// Tupla nominata Position in modo che nel codice sia più chiaro a cosa serve.\ /// È molto più facile capire a colpo d'occhio Position rispetto a (usize, usize)\ /// I due valori sono la posizione sull'asse X e sull'asse Y\ /// Il punto (0,0) si trova in basso a sinista. -#[derive(Clone, Copy, Deserialize, Serialize)] +#[derive(PartialEq, Eq, Clone, Copy, Deserialize, Serialize)] pub struct Position(pub usize, pub usize); /// Indica la direzione dove una entità sta guardando.\ /// È possibile anche non guardare in nessuna direzione tramite None. -#[derive(Clone, Deserialize, Serialize)] +#[derive(PartialEq, Eq, Clone, Copy, Deserialize, Serialize)] pub enum Direction { Up, Down, @@ -60,19 +63,22 @@ impl Direction { _ => Direction::None, } } -} - -impl Display for Direction { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let c = match self { + /// Restituisce la rappresentazione della direzione in formato char, in questo modo + /// può essere utilizzata per vedere il valore e mostrarlo a terminale. + pub fn as_char(&self) -> char { + match self { Self::Up => '▲', Self::Down => '▼', Self::Left => '◄', Self::Right => '►', Self::None => '■', - }; + } + } +} - write!(f, "{}", c) +impl Display for Direction { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.as_char()) } } @@ -81,7 +87,7 @@ impl Display for Direction { pub struct Entity { name: String, effects: VecDeque>, - decider: Box, + behavior: Box, floor: FloorPtr, pub buffer: Action, pub position: Position, @@ -100,14 +106,14 @@ impl Entity { name: String, health: i32, attack: i32, - decider: Box, + behavior: Box, mut floor: FloorPtr, ) -> Self { let position = floor.get().get_entrance(); Self { name, floor, - decider, + behavior, position, attack, health, @@ -177,6 +183,14 @@ impl Entity { false } + /// Permette all'entità di mostrare il piano in cui si trova e di fare una mossa.\ + /// Ha la stessa funzionalità di update() ma prima mostra il piano dell'entità.\ + /// Il piano viene mostrato tramite il behavior dell'entità. + pub fn update_display(&mut self, floor: FloorView) -> bool { + self.behavior.update(floor); + self.update() + } + /// calcola gli effetti e li applica all'entità. fn compute_effects(&mut self) { let total = self.effects.len(); // len could change @@ -189,7 +203,7 @@ impl Entity { /// prende una decisione e applica l'azione da fare /// L'azione compiuta viene restituita, altrimenti None fn compute_action(&mut self) -> Option { - let action = self.decider.get_next_action()?; + let action = self.behavior.get_next_action()?; let action = match self.buffer { Action::DoNothing => action, _ => mem::replace(&mut self.buffer, Action::DoNothing), @@ -199,20 +213,22 @@ impl Entity { action.apply(self); result } +} - /// Metodo statico per l'update e l'eventuale eliminazione di entità da un vettore. - /// Le entità rimosse sono quelle che non riescono a fare l'update o che eventualmente - /// non sono più in vita - pub fn update_from_vec(entities: &mut Vec) { - let to_remove: Vec = entities - .iter_mut() - .enumerate() - .filter_map(|(i, entity)| if entity.update() { Some(i) } else { None }) - .rev() - .collect(); - to_remove.iter().for_each(|i| { - entities.remove(*i); - }); +impl Display for Entity { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let times = 20; + let health_bar = (self.health * times) / self.health_max; + + let filled = "█".repeat(health_bar as usize); + let empty = " ".repeat((times - health_bar) as usize); + let health_bar = format!("[{}{}]", filled, empty); + + write!( + f, + "{}: {} {}{:4}/{:4}", + self.name, self.direction, health_bar, self.health, self.health_max + ) } } @@ -255,17 +271,22 @@ impl Action { /// \ /// Il trait è taggato con typetag in modo che possa essere utilizzato /// nella serializzazione e deserializzazione di serde. -/// Esso permette di trasformare le implementazioni di Decider in una +/// Esso permette di trasformare le implementazioni di questo trait in una /// spiecie di Enum senza il bisogno di farlo manualmente.\ /// Quello che viene richiesto è che, nell'implementazione di una -/// struttura concreta di questo trait, venga messo sopra impl X for Decider:\ +/// struttura concreta di questo trait, venga messo sopra impl X for Behavior:\ /// #\[typetag::serde\]\ /// \ /// In questo modo si possono creare molteplici comoprtamenti che implementano /// questo trait senza il bisogno di avere un Enum con essi #[typetag::serde(tag = "type")] -pub trait Decider: DynClone { - /// Genera una azione che poi verrà usata per l'entità associata a questo Decider.\ +pub trait Behavior: DynClone { + /// In questo metodo viene passata una struttura che contiene una rappresentazione del + /// piano semplice, avente solo delle informazioni parziali.\ + /// Questo serve a mostrare eventualmente delle possibili informazioni all'utente + /// o di registrare dei valori per l'algoritmo di generazione delle azioni. + fn update(&self, floor: FloorView); + /// Genera una azione che poi verrà usata per l'entità associata.\ /// L'azione può essere generata in qualunque modo: casuale, sempre la stessa, /// tramite interazione con console, o tramite una connessione ad un client.\ /// \ @@ -274,15 +295,45 @@ pub trait Decider: DynClone { /// ma anche una possibilità che alcune entità rare possano sparire. fn get_next_action(&self) -> Option; } -clone_trait_object!(Decider); +clone_trait_object!(Behavior); /// Semplice implementazione di un possibile comportamento di una entità.\ /// In questo caso l'entità resterà immobile nel punto in cui si trova per sempre. #[derive(Clone, Serialize, Deserialize)] pub struct Immovable; #[typetag::serde] -impl Decider for Immovable { +impl Behavior for Immovable { + fn update(&self, _floor: FloorView) {} fn get_next_action(&self) -> Option { Some(Action::DoNothing) } } + +/// Semplice implementazione di una possibile interfaccia console. +#[derive(Clone, Serialize, Deserialize)] +pub struct ConsoleInput; +#[typetag::serde] +impl Behavior for ConsoleInput { + fn update(&self, floor: FloorView) { + let mut term = console::Term::stdout(); + let _ = term.clear_screen(); + let _ = term.write_fmt(format_args!("{}\n", floor)); + } + fn get_next_action(&self) -> Option { + let mut term = console::Term::stdout(); + let _ = term.write("Insert your action [wasd or enter for nothing]: ".as_bytes()); + + loop { + if let Ok(ch) = term.read_char() { + match ch { + '\n' => return Some(Action::DoNothing), + 'w' => return Some(Action::Move(Direction::Up)), + 'a' => return Some(Action::Move(Direction::Left)), + 's' => return Some(Action::Move(Direction::Down)), + 'd' => return Some(Action::Move(Direction::Right)), + _ => (), + } + } + } + } +} diff --git a/src/es03_game/floor.rs b/src/es03_game/floor.rs index 6090c9a..1d820d1 100644 --- a/src/es03_game/floor.rs +++ b/src/es03_game/floor.rs @@ -6,6 +6,7 @@ use rand_pcg::Pcg32; use serde::{Deserialize, Serialize}; use std::{ cell::{RefCell, RefMut}, + fmt::Display, rc::Rc, }; @@ -19,9 +20,13 @@ impl FloorPtr { /// Il piano viene creato a partire dai parametri passati in input, che sono tutte cose /// necessarie ad esso. pub fn new(level: usize, rng: Pcg32, entities: Vec, grid: Vec>) -> Self { - Self(Rc::new(RefCell::new(Floor::new( - level, rng, entities, grid, - )))) + Self(Rc::new(RefCell::new(Floor { + level, + rng, + players: vec![], + entities, + grid, + }))) } /// Permette di prendere il valore puntato al piano. @@ -37,21 +42,12 @@ impl FloorPtr { pub struct Floor { level: usize, grid: Vec>, + players: Vec, entities: Vec, rng: Pcg32, } impl Floor { - /// Crea un piano secondo i parametri indicati - fn new(level: usize, rng: Pcg32, entities: Vec, grid: Vec>) -> Self { - Self { - level, - grid, - entities, - rng, - } - } - /// Restituisce il livello di profondità del piano pub fn get_level(&self) -> usize { self.level @@ -89,6 +85,100 @@ impl Floor { /// Fa l'update di tutte le entità e rimuove eventualmente quelle non più in vita pub fn update_entities(&mut self) { - Entity::update_from_vec(&mut self.entities); + let to_remove: Vec = self + .entities + .iter_mut() + .map(|entity| entity.update()) + .collect(); + let mut to_remove = to_remove.iter(); + self.entities.retain(|_| *to_remove.next().unwrap()); + } +} + +/// Struttura di mezzo tra un piano e il gioco vero e proprio.\ +/// Utilizzata per la comunicazione con le entità per poter aggiornare quello che vedono.\ +/// Infatti internamente ha solo alcuni pezzi del gioco per non far mostrare tutto.\ +pub struct FloorView { + pub level: usize, + pub entity: Entity, + pub players: Vec, + pub entities: Vec, + pub grid: Vec>, +} + +impl FloorView { + /// Crea una vista del gioco corrente secondo la visione dell entità passata in intput.\ + /// Il SimpleFloor risultante avrà il piano, entità, livello e giocatori che si trovano + /// in questo momento sul piano dell'entità passata in input. + pub fn new(entity: &Entity) -> Self { + let mut floor = entity.get_floor(); + let floor = floor.get(); + + let level = floor.level; + let grid = floor.grid.clone(); + let entities: Vec = floor + .entities + .iter() + .filter_map(|e| (e.position != entity.position).then_some(e.clone())) + .collect(); + let players: Vec = floor + .players + .iter() + .filter_map(|p| (p.position != entity.position).then_some(p.clone())) + .collect(); + + Self { + level, + entity: entity.clone(), + players, + entities, + grid, + } + } + + /// Rappresentazione del piano come matrice di char + pub fn as_char_grid(&self) -> Vec> { + let size = self.grid.len(); + let mut grid: Vec> = (0..size) + .map(|y| { + let row = (0..size).map(|x| Some(&self.grid[x][y])); + let mut row: Vec<_> = row + .clone() + .zip(row.skip(1).chain(std::iter::once(None))) + .flat_map(|(a, b)| { + let a = a.unwrap(); + if let Some(b) = b { + let one_is_wall = matches!(b, Cell::Wall) || matches!(a, Cell::Wall); + let c = if one_is_wall { Cell::Wall } else { Cell::Empty }; + vec![a.as_char(), c.as_char()] + } else { + vec![a.as_char()] + } + }) + .collect(); + row.push('\n'); + row + }) + .collect(); + + let pos = &self.entity.position; + grid[pos.1][pos.0 * 2] = self.entity.direction.as_char(); + grid + } +} + +impl Display for FloorView { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let grid: String = self + .as_char_grid() + .iter() + .rev() + .map(|row| { + let a: String = row.iter().collect(); + a + }) + .collect(); + + write!(f, "{}\n{}", grid, self.entity) } } diff --git a/src/es03_game/game.rs b/src/es03_game/game.rs index cb67cc2..0fd00f8 100644 --- a/src/es03_game/game.rs +++ b/src/es03_game/game.rs @@ -1,8 +1,8 @@ use super::{ cell::Cell, config::Config, - entities::{Entity, Immovable}, - floor::FloorPtr, + entities::{Behavior, Entity}, + floor::{FloorPtr, FloorView}, generator::Generator, }; use rand::{RngCore, SeedableRng}; @@ -41,14 +41,25 @@ impl Dungeon { /// Aggiunge un giocatore al Dungeon, esso avrà le statistiche di base assegnate /// ad esso tramite la configurazione indicata nel costruttore.\ /// Il giocatore appena inserito si troverà al piano 0. - pub fn add_player(&mut self, name: String) { + pub fn add_player(&mut self, name: String, decider: Box) { let floor = self.floors[0].clone(); - let decider = Box::new(Immovable); let stats = &self.config.player_stats; let entity = Entity::new(name, stats.health, stats.attack, decider, floor); self.players.push(entity); } + /// Indica se nel dungeon ci sono dei giocatori.\ + /// Metodo utile, dato che nel caso in cui non ci siano, il dungen non verrà modificato + /// siccome per calcolare il turno successivo ho bisogno di giocatori. + pub fn has_players(&self) -> bool { + !self.get_players().is_empty() + } + + /// Restiutuisce la lista dei giocatori che in questo momento stanno giocando. + pub fn get_players(&self) -> &Vec { + &self.players + } + /// Restituisce il piano indicato dal livello di profondità.\ /// Nel caso il livello non esista, restituisce il piano con profondità maggiore. pub fn get_floor(&self, level: usize) -> FloorPtr { @@ -65,33 +76,13 @@ impl Dungeon { /// - Update di tutti i piani in cui c'è almeno un giocatore /// - Modifica di piano di eventuali giocatori pub fn compute_turn(&mut self) { - let mut update_floors = vec![false; self.floors.len()]; - let mut change_floors = vec![0; self.players.len()]; + if !self.has_players() { + return; + } - Entity::update_from_vec(&mut self.players); - self.players.iter_mut().enumerate().for_each(|(i, player)| { - let mut floor = player.get_floor(); - let mut floor = floor.get(); - update_floors[floor.get_level()] = true; - if let Cell::Exit = floor.get_cell(player.position) { - change_floors[i] = floor.get_level() + 1; - } - }); - - update_floors - .iter() - .enumerate() - .filter_map(|(i, b)| if *b { Some(i) } else { None }) - .for_each(|i| self.floors[i].get().update_entities()); - change_floors - .iter() - .enumerate() - .filter(|(_, f)| **f != 0) - .for_each(|(player, floor)| { - let floor = self.get_floor_or_build(*floor); - let player = &mut self.players[player]; - player.set_floor(floor); - }); + let mut update = UpdateDungeon::compute(self); + update.update_floors(); + update.remove_eventual_players(); } /// permette di costruire il piano successivo @@ -115,3 +106,81 @@ impl Dungeon { self.get_floor(level) } } + +/// Serve al dungeon per fare tutti i vari update.\ +/// È stata creata una struttura e una sua implementazione apposta dato che per gli update la logica +/// è sia complessa che contorta. In questo modo si riesce a capire meglio che cosa viene fatto +/// utilizzando delle funzioni apposta. +struct UpdateDungeon<'a> { + dungeon: &'a mut Dungeon, + players: Vec, + update_floors: Vec, + change_floors: Vec, +} + +impl<'a> UpdateDungeon<'a> { + /// Crea un update del dungeon.\ + /// Con questo metodo si inizializza la struttura e per farlo viene chiamata la funzione + /// update_display per ogni player che il dungeon ha attivo.\ + /// Dopo questo metodo, la struttura che ne risulta ha salvato alcuni parametri che possono + /// diventare obsoleti nel caso in cui i metodo vangano chiamati dopo troppo tempo. + fn compute(dungeon: &'a mut Dungeon) -> Self { + let mut update_floors = vec![false; dungeon.floors.len()]; + let mut change_floors = vec![0; dungeon.players.len()]; + + let players: Vec = (0..dungeon.players.len()) + .into_iter() + .map(|i| { + let player = &mut dungeon.players[i]; + let floor = FloorView::new(&player); + let value = player.update_display(floor); + + let mut floor = player.get_floor(); + let mut floor = floor.get(); + update_floors[floor.get_level()] = true; + if let Cell::Exit = floor.get_cell(player.position) { + change_floors[i] = floor.get_level() + 1; + } + + value + }) + .collect(); + Self { + dungeon, + players, + update_floors, + change_floors, + } + } + + /// Permette di fare l'update dei tutti i piani che hanno giocatori attivi.\ + /// I giocatori attivi vengono calcolati appena viene creata l'istanza, quindi + /// questo metodo può diventare obsoleto nel caso in cui venga chiamato dopo troppo tempo + /// dall'inizializzazione della struttura. + fn update_floors(&mut self) -> &mut Self { + self.update_floors + .iter() + .enumerate() + .filter_map(|(i, b)| (*b).then(|| i)) + .for_each(|i| self.dungeon.floors[i].get().update_entities()); + self.change_floors + .iter() + .enumerate() + .filter(|(_, f)| **f != 0) + .for_each(|(player, floor)| { + let floor = self.dungeon.get_floor_or_build(*floor); + let player = &mut self.dungeon.players[player]; + player.set_floor(floor); + }); + self + } + + /// Permette di rimuovare eventuali giocatori che non servono più.\ + /// Questo metodo prende l'ownership della struttura dato che deve essere chiamato per ultimo + /// siccome può modificare la lunghezza del vettore di players, invalidando quindi + /// tutti i parametri creati precedentemente. + fn remove_eventual_players(self) { + let mut players = self.players.iter(); + self.dungeon.players.retain(|_| *players.next().unwrap()); + } +} diff --git a/src/es03_game/generator.rs b/src/es03_game/generator.rs index e357a0b..b707182 100644 --- a/src/es03_game/generator.rs +++ b/src/es03_game/generator.rs @@ -37,13 +37,9 @@ impl Generator { size: floor_size, size_rooms: config.room_size.clone(), effects_total: config.effects_total, - effects: Self::vec_filter(&config.effects, |val| { - val.floors.contains(&floor_level) - }), + effects: Self::vec_filter(&config.effects, |val| val.floors.contains(&floor_level)), entities_total: config.entities_total, - entities: Self::vec_filter(&config.entities, |val| { - val.floors.contains(&floor_level) - }), + entities: Self::vec_filter(&config.entities, |val| val.floors.contains(&floor_level)), grid: Self::grid_with_only(floor_size, Cell::Wall), } } @@ -122,7 +118,7 @@ impl Generator { original .clone() .into_iter() - .filter_map(|val| if filter(&val) { Some(val) } else { None }) + .filter_map(|val| filter(&val).then(|| val)) .collect() } /// crea un campo con solamente la cella specificata clonata su tutto di esso