diff --git a/src/es03_game/config.rs b/src/es03_game/config.rs index 06a62d9..914ba7a 100644 --- a/src/es03_game/config.rs +++ b/src/es03_game/config.rs @@ -1,6 +1,6 @@ use super::{ cell::{Confusion, Effect, InstantDamage}, - entities::Behavior, + entities::{Behavior, RandomMovement}, }; use serde::{Deserialize, Serialize}; use std::ops::Range; @@ -65,7 +65,7 @@ pub struct ConfigPlayer { pub struct ConfigEntity { pub floors: Range, pub name: String, - pub decider: Box, + pub behavior: Box, pub health: i32, pub attack: i32, pub priority: u32, @@ -96,12 +96,19 @@ impl Default for Config { ConfigEffect { effect: Box::new(Confusion(10)), floors: 0..255, - priority: 1, + priority: 10, }, ], effects_total: 45, - entities: vec![], - entities_total: 0, + entities: vec![ConfigEntity { + floors: 0..255, + name: "Basic enemy".to_string(), + behavior: Box::new(RandomMovement::new()), + health: 30, + attack: 10, + priority: 1, + }], + entities_total: 10, player_stats: ConfigPlayer { health: 100, attack: 10, diff --git a/src/es03_game/entities.rs b/src/es03_game/entities.rs index 7696638..8dfbbb9 100644 --- a/src/es03_game/entities.rs +++ b/src/es03_game/entities.rs @@ -1,9 +1,9 @@ use super::{ - cell::Effect, + cell::{Cell, Effect}, floor::{Floor, FloorView}, }; use dyn_clone::{clone_trait_object, DynClone}; -use rand::Rng; +use rand::{Rng, SeedableRng}; use rand_pcg::Pcg32; use serde::{Deserialize, Serialize}; use std::{collections::VecDeque, fmt::Display, mem}; @@ -17,12 +17,13 @@ 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(PartialEq, Eq, Hash, Clone, Copy, Debug, Deserialize, Serialize)] +#[derive(PartialEq, Eq, Hash, Clone, Copy, Default, Debug, Deserialize, Serialize)] pub enum Direction { Up, Down, Left, Right, + #[default] None, } @@ -89,7 +90,7 @@ impl Display for Direction { pub struct Entity { name: String, effects: VecDeque>, - behavior: Box, + behavior: Option>, pub buffer: Action, pub position: Position, pub direction: Direction, @@ -106,7 +107,7 @@ impl Entity { pub fn new(name: String, health: i32, attack: i32, behavior: Box) -> Self { Self { name, - behavior, + behavior: Some(behavior), position: Position(0, 0), attack, health, @@ -164,33 +165,41 @@ impl Entity { /// Permette all'entità di mostrare il piano in cui si trova e di fare una mossa.\ /// Il piano viene mostrato tramite il behavior dell'entità e successivamente viene chiesto di fare un'azione.\ /// Dopodichè vengono calcolati tutti gli effetti che devono essere applicati all'entità.\ - /// Nel caso in cui l'entità non sia più in vita questo metodo ritornerà false - /// e non permetterà all'entità di fare update.\ - /// Nel caso in cui l'entità non riesca a fare l'update viene ritornato false.\ + /// Nel caso in cui l'entità non sia più in vita questo metodo ritornerà None + /// e l' entità smetterà di esistere.\ + /// Nel caso in cui l'entità non riesca a fare l'update viene ritornato None.\ /// Cio significa che l'entità verrà rimossa dal gioco. - pub fn update(&mut self, floor: &mut Floor) -> bool { + pub fn update(mut self, floor: &mut Floor) -> Option { + let mut behavior = mem::take(&mut self.behavior).unwrap(); + if !self.is_alive() { - self.behavior.you_died(floor.get_limited_view_floor(self)); - return false; + return self.die(behavior, floor); } - self.behavior.update(floor.get_limited_view_floor(self)); - let action = self.compute_action(floor); + behavior.update(floor.get_limited_view_floor(&self)); + let action = self.compute_action(&mut behavior, floor); if action.is_none() { - return false; + return None; } if !self.is_alive() { - self.behavior.you_died(floor.get_limited_view_floor(self)); - return false; + return self.die(behavior, floor); } self.compute_effects(floor); if !self.is_alive() { - self.behavior.you_died(floor.get_limited_view_floor(self)); - return false + return self.die(behavior, floor); } - true + + self.behavior = Some(behavior); + Some(self) + } + + /// metodo usato per la rimozione dell' entità e del suo behavior + fn die(self, mut behavior: Box, floor: &Floor) -> Option { + let view = floor.get_limited_view_floor(&self); + behavior.on_death(view); + None } /// calcola gli effetti e li applica all'entità. @@ -204,8 +213,8 @@ impl Entity { } /// prende una decisione e applica l'azione da fare /// L'azione compiuta viene restituita, altrimenti None - fn compute_action(&mut self, floor: &mut Floor) -> Option { - let action = self.behavior.get_next_action()?; + fn compute_action(&mut self, behavior: &mut Box, floor: &mut Floor) -> Option { + let action = behavior.get_next_action()?; let action = match self.buffer { Action::DoNothing => action, _ => mem::replace(&mut self.buffer, Action::DoNothing), @@ -236,10 +245,11 @@ impl Display for Entity { /// Azione che una qualsiasi entità può fare. /// L'azione DoNothing permette all'entità di saltare il turno nel caso in cui sia utile. -#[derive(Clone, Debug, Deserialize, Serialize)] +#[derive(Clone, Default, Debug, Deserialize, Serialize)] pub enum Action { Move(Direction), //Attack(Direction), + #[default] DoNothing, } @@ -257,7 +267,7 @@ impl Action { direction.move_from(&mut entity.position); entity.direction = direction; - let cell = floor.get_cell(&entity.position); + let cell = floor.get_cell_mut(&entity.position); cell.entity_over(entity); } } @@ -283,12 +293,14 @@ pub trait Behavior: DynClone + core::fmt::Debug { /// 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); + /// o di registrare dei valori per l'algoritmo di generazione delle azioni.\ + /// Non è necessario implementarla. + fn update(&mut self, _view: FloorView) {} /// Funzione che viene richiamata quando l'entità muore.\ /// I parametri servono a far vedere un'ultima volta i dati del piano corrente all'entità - /// in modo che possa eventualmente fare ulteriori calcoli. - fn you_died(&self, floor: FloorView); + /// in modo che possa eventualmente fare ulteriori calcoli.\ + /// Non è necessario implementarla. + fn on_death(&mut self, _view: 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.\ @@ -296,7 +308,7 @@ pub trait Behavior: DynClone + core::fmt::Debug { /// Nel caso in cui venga restituito None come valore, l'entità verrà rimossa dal gioco.\ /// Questo viene fatto in modo che si possa avere una possibilità di rimozione del giocatore, /// ma anche una possibilità che alcune entità rare possano sparire. - fn get_next_action(&self) -> Option; + fn get_next_action(&mut self) -> Option; } clone_trait_object!(Behavior); @@ -306,9 +318,37 @@ clone_trait_object!(Behavior); pub struct Immovable; #[typetag::serde] impl Behavior for Immovable { - fn update(&self, _floor: FloorView) {} - fn you_died(&self, _floor: FloorView) {} - fn get_next_action(&self) -> Option { + fn get_next_action(&mut self) -> Option { Some(Action::DoNothing) } } + +/// Semplice implementazione di un possibile comportamento di una entità.\ +/// In questo caso l'entità si mouverà in maniera casuale evitando le caselle speciali. +#[derive(Clone, Debug, Serialize, Deserialize)] +pub struct RandomMovement { + action: Action, + rng: Pcg32, +} +impl RandomMovement { + pub fn new() -> Self { + Self { + action: Action::DoNothing, + rng: Pcg32::seed_from_u64(0), + } + } +} +#[typetag::serde] +impl Behavior for RandomMovement { + fn update(&mut self, view: FloorView) { + let dir = Direction::random(&mut self.rng); + let mut pos = view.entity.position.clone(); + dir.move_from(&mut pos); + if let Cell::Empty = view.floor.get_cell(&pos) { + self.action = Action::Move(dir); + } + } + fn get_next_action(&mut self) -> Option { + Some(mem::take(&mut self.action)) + } +} diff --git a/src/es03_game/floor.rs b/src/es03_game/floor.rs index 062c0fc..db828eb 100644 --- a/src/es03_game/floor.rs +++ b/src/es03_game/floor.rs @@ -68,13 +68,25 @@ impl Floor { /// Nel caso in cui la posizione non sia all'interno del piano, essa viene modificata /// facendola rientrare nei limiti di esso.\ /// Es. pos(2,3) ma il piano è di max 2 allora diventa -> pos(2,2) - pub fn get_cell(&mut self, pos: &Position) -> &mut Cell { + pub fn get_cell_mut(&mut self, pos: &Position) -> &mut Cell { let len = self.grid.len() - 1; let x = pos.0.min(len); let y = pos.1.min(len); &mut self.grid[x][y] } + /// Restituisce la cella nella posizione indicata.\ + /// Con essa si può leggere la cella senza però la possibilità di modificarla.\ + /// Nel caso in cui la posizione non sia all'interno del piano, essa viene modificata + /// facendola rientrare nei limiti di esso.\ + /// Es. pos(2,3) ma il piano è di max 2 allora diventa -> pos(2,2) + pub fn get_cell(&self, pos: &Position) -> &Cell { + let len = self.grid.len() - 1; + let x = pos.0.min(len); + let y = pos.1.min(len); + &self.grid[x][y] + } + /// Restituisce la posizione dell'entrata del piano.\ /// Utile come spawn per quando i giocatori arrivano al piano. pub fn get_entrance(&mut self) -> Position { @@ -93,20 +105,6 @@ impl Floor { .expect("Entrance of the floor should be inside the grid!") } - /// Fa l'update di tutti i giocatori e rimuove eventualmente quelli non più in vita, restituendoli dentro un vec - pub fn update_players(&mut self) -> Vec { - let mut next_floor = vec![]; - for _ in 0..self.players.len() { - let mut player = self.players.pop_front().unwrap(); - if player.update(self) { - self.players.push_back(player); - } else { - next_floor.push(player); - } - } - next_floor - } - /// Ritorna un eventuale giocatore che si trova sopra la cella di uscita del piano.\ /// Nel caso in cui non ci siano giocatori sopra, questo metodo ritornerà None. pub fn get_player_at_exit(&mut self) -> Option { @@ -130,11 +128,21 @@ impl Floor { } } + /// Fa l'update di tutti i giocatori e rimuove quelli non più in vita + pub fn update_players(&mut self) { + for _ in 0..self.players.len() { + let player = self.players.pop_front().unwrap(); + if let Some(player) = player.update(self) { + self.players.push_back(player); + } + } + } + /// Fa l'update di tutte le entità e rimuove eventualmente quelle non più in vita pub fn update_entities(&mut self) { for _ in 0..self.entities.len() { - let mut entity = self.entities.pop_front().unwrap(); - if entity.update(self) { + let entity = self.entities.pop_front().unwrap(); + if let Some(entity) = entity.update(self) { self.entities.push_back(entity); } } diff --git a/src/es03_game/game.rs b/src/es03_game/game.rs index 203fa47..ad79da3 100644 --- a/src/es03_game/game.rs +++ b/src/es03_game/game.rs @@ -23,7 +23,7 @@ pub struct Dungeon { } impl Dungeon { - /// Crea una nuova istanza di un dungeon con le configurazioni i default + /// Crea una nuova istanza di un dungeon con le configurazioni di default pub fn new() -> Self { Self::new_with(Config::default()) } diff --git a/src/es03_game/generator.rs b/src/es03_game/generator.rs index 3cab6b9..b506db9 100644 --- a/src/es03_game/generator.rs +++ b/src/es03_game/generator.rs @@ -74,16 +74,20 @@ impl<'a> Generator<'a> { self.rand_place_effects(&mut grid); Floor::new(self.level, self.rng, vec![], grid) } + + /// todo!() docs + fn rand_place_entities(&mut self, grid: &mut Vec>) { + todo!() + } /// piazza gli effetti della confgurazione in modo casuale su tutto il piano.\ /// essi vengono piazzati solamente sulle celle Empty fn rand_place_effects(&mut self, grid: &mut Vec>) { - let total = self.config.effects_total; - let original = &self.config.effects; - let effects = Self::vec_filter(original, |val| val.floors.contains(&self.level)); + let effects = vec_filter(&self.config.effects, |e| { + e.floors.contains(&self.level).then(|| (e.priority, e)) + }); - for _ in 0..total { - let index = self.rng.gen_range(0..effects.len()); - let effect = effects[index].effect.clone(); + for _ in 0..self.config.effects_total { + let effect = vec_get_sample(&effects, &mut self.rng).effect.clone(); let cell = Cell::Special(effect); self.rand_place(grid, cell, 0..self.size, 0..self.size); } @@ -106,14 +110,29 @@ impl<'a> Generator<'a> { } } } - /// crea una vista del vettore passato in input dopo aver applicato la funzione di filtro - fn vec_filter(original: &Vec, filter: impl Fn(&T) -> bool) -> Vec { - original - .clone() - .into_iter() - .filter_map(|val| filter(&val).then(|| val)) - .collect() - } +} + +/// crea una vista del vettore passato in input dopo aver applicato la funzione di filtro +pub fn vec_filter(original: &Vec, filter: F) -> Vec<(f32, &T)> +where + F: FnMut(&T) -> Option<(u32, &T)>, +{ + let temp = original.iter().filter_map(filter).collect::>(); + let max = temp.iter().fold(0, |a, b| a.max(b.0)) + 1; + let total = temp.iter().map(|(p, _)| (max - *p) as f32).sum::(); + let mut accum = 0.0; + temp.into_iter() + .map(|(p, item)| { + accum += (max - p) as f32 / total; + (accum, item) + }) + .collect() +} + +/// todo!() docs +pub fn vec_get_sample<'a, T>(vec: &Vec<(f32, &'a T)>, rng: &mut Pcg32) -> &'a T { + let sample = rng.gen_range(0.0..1.0); + vec.iter().filter(|(p, _)| *p >= sample).next().unwrap().1 } /// Utile per la generazione del labirinto.\ diff --git a/src/es03_game/mod.rs b/src/es03_game/mod.rs index db53c94..d91372f 100644 --- a/src/es03_game/mod.rs +++ b/src/es03_game/mod.rs @@ -56,10 +56,41 @@ pub fn run_console(player: String, seed: u64) { game.add_player(player, Box::new(ConsoleInput)); while game.has_players() { + let _ = game.save("save.json"); game.compute_turn(); } } +/// todo!() add docs +pub fn box_of( + size: usize, + title: String, + iter: impl Iterator, +) -> impl Iterator { + assert!( + size >= title.len(), + "Title must not exceed the size of the box!" + ); + + let len = (size - title.len()) / 2; + let correction = if 2 * len + title.len() < size { 1 } else { 0 }; + + std::iter::once("╔".to_string()) + .chain(std::iter::repeat("═".to_string()).take(len + 1)) + .chain(std::iter::once(title)) + .chain(std::iter::repeat("═".to_string()).take(len + 1 + correction)) + .chain(std::iter::once("╗\n".to_string())) + .chain(iter.map(|string| { + std::iter::once("║ ".to_string()) + .chain(std::iter::once(string)) + .chain(std::iter::once(" ║\n".to_string())) + .collect() + })) + .chain(std::iter::once("╚".to_string())) + .chain(std::iter::repeat("═".to_string()).take(size + 2)) + .chain(std::iter::once("╝\n".to_string())) +} + const COLOR_RESET: &str = "\x1b[0m"; const COLOR_EFFECT: &str = "\x1b[95m"; const COLOR_ENEMY: &str = "\x1b[38;5;1m"; @@ -77,9 +108,8 @@ impl ConsoleInput { let mut term = console::Term::stdout(); let _ = term.clear_screen(); let _ = term.write_fmt(format_args!( - "{}Floor lv.{:2} - {}\n{other}\n", + "{}{}\n{other}\n", Self::floor_as_string(&floor), - floor.floor.get_level(), Self::entity_as_string(floor.entity), )); } @@ -122,33 +152,19 @@ impl ConsoleInput { .collect() }); - Self::box_of(size, iter).collect() - } - /// todo!() add docs - fn box_of(size: usize, iter: impl Iterator) -> impl Iterator { - std::iter::once("╔".to_string()) - .chain(std::iter::repeat("═".to_string()).take(size + 2)) - .chain(std::iter::once("╗\n".to_string())) - .chain(iter.map(|string| { - std::iter::once("║ ".to_string()) - .chain(std::iter::once(string)) - .chain(std::iter::once(" ║\n".to_string())) - .collect() - })) - .chain(std::iter::once("╚".to_string())) - .chain(std::iter::repeat("═".to_string()).take(size + 2)) - .chain(std::iter::once("╝\n".to_string())) + let title = format!(" Floor lv.{:2} ", floor.floor.get_level()); + box_of(size, title, iter).collect() } } #[typetag::serde] impl Behavior for ConsoleInput { - fn update(&self, floor: FloorView) { + fn update(&mut self, floor: FloorView) { self.print_floor(floor, "".to_string()); } - fn you_died(&self, floor: FloorView) { + fn on_death(&mut self, floor: FloorView) { self.print_floor(floor, "YOU DIED!".to_string()); } - fn get_next_action(&self) -> Option { + fn get_next_action(&mut self) -> Option { let mut term = console::Term::stdout(); let _ = term.write("Insert your action [wasd or space for nothing]: ".as_bytes());