From 8862941152bff29c786834878a477dcc0efe5ba3 Mon Sep 17 00:00:00 2001 From: Berack96 Date: Sat, 18 May 2024 17:49:23 +0200 Subject: [PATCH] Maze Genetation - added maze gen - refactored Generator - added config for maze gen - changed floor display to 3x instead of 2x --- src/es03_game/cell.rs | 5 +- src/es03_game/config.rs | 32 ++- src/es03_game/entities.rs | 36 ++- src/es03_game/floor.rs | 49 ++-- src/es03_game/generator.rs | 552 +++++++++++++++++++++++++++++++------ 5 files changed, 540 insertions(+), 134 deletions(-) diff --git a/src/es03_game/cell.rs b/src/es03_game/cell.rs index fc60384..571f800 100644 --- a/src/es03_game/cell.rs +++ b/src/es03_game/cell.rs @@ -34,7 +34,7 @@ impl Cell { } Cell::Wall => { entity.direction.invert(); - entity.position = entity.direction.move_from(entity.position); + entity.direction.move_from(&mut entity.position); } _ => (), } @@ -115,8 +115,7 @@ impl Effect for Confusion { fn apply_to(&self, entity: &mut Entity, floor: &mut Floor) { if self.0 > 0 { let rng = floor.get_rng(); - let coin_flip = rng.gen_range(0..=1); - if coin_flip == 1 { + if rng.gen_bool(0.5) { let random_direction = Direction::random(rng); entity.buffer = Action::Move(random_direction); } diff --git a/src/es03_game/config.rs b/src/es03_game/config.rs index d79b7c7..bdace44 100644 --- a/src/es03_game/config.rs +++ b/src/es03_game/config.rs @@ -7,13 +7,11 @@ use std::ops::Range; /// Struttura di configurazione per la creazione di un dungeon.\ /// Ogni elemento indica un parametro per la generazione di un piano o di una entitità.\ -/// Esiste una implementazione di default di questa struttura che genera un dungeon -/// molto semplice e standard. +/// Esiste una implementazione di default di questa struttura che genera un dungeon standard. #[derive(Clone, Deserialize, Serialize)] pub struct Config { pub game_seed: u64, - pub room_size: Range, - pub floor_size: Range, + pub maze_generation: ConfigMaze, pub effects_total: usize, pub effects: Vec, pub entities_total: usize, @@ -21,6 +19,21 @@ pub struct Config { pub player_stats: ConfigPlayer, } +/// Configura la generazione del labirinto all'interno del generatore.\ +/// I parametri principali servono ad indicare quanto grande è il piano e quanto grandi sono le stanze.\ +/// *room_placing_attempts* indica quanti tentativi il generatore deve fare prima di smettere di creare stanze.\ +/// *straight_percentage* indica da 0 a 100 quanta percentuale c'è che un corridioio, quando viene generato +/// rimanga dritto o viri.\ +/// *dead_ends* indica quanti corridoi che non portano a nulla devono esserci alla fine della generazione. +#[derive(Clone, Deserialize, Serialize)] +pub struct ConfigMaze { + pub floor_size: Range, + pub room_size: Range, + pub room_placing_attempts: u32, + pub straight_percentage: u32, + pub dead_ends: u32, +} + /// Un effetto che si può trovare per terra nel dungeon.\ /// La priorità indica quanto verrà spawnato l'effetto in media.\ /// \ @@ -62,8 +75,13 @@ impl Default for Config { fn default() -> Self { Self { game_seed: 0, - room_size: 5..10, - floor_size: 30..40, + maze_generation: ConfigMaze { + floor_size: 30..40, + room_size: 5..10, + room_placing_attempts: 10, + straight_percentage: 90, + dead_ends: 0, + }, effects: vec![ ConfigEffect { effect: Box::new(InstantDamage(20)), @@ -87,7 +105,7 @@ impl Default for Config { player_stats: ConfigPlayer { health: 1000, attack: 100, - } + }, } } } diff --git a/src/es03_game/entities.rs b/src/es03_game/entities.rs index 4a9fba3..c3b62b7 100644 --- a/src/es03_game/entities.rs +++ b/src/es03_game/entities.rs @@ -12,12 +12,12 @@ use std::{collections::VecDeque, fmt::Display, io::Write, mem}; /// È 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(PartialEq, Eq, Clone, Copy, Deserialize, Serialize)] +#[derive(PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Debug, 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(PartialEq, Eq, Clone, Copy, Deserialize, Serialize)] +#[derive(PartialEq, Eq, Hash, Clone, Copy, Deserialize, Serialize)] pub enum Direction { Up, Down, @@ -38,18 +38,20 @@ impl Direction { _ => Direction::None, }; } - /// Calcola la nuova posizione in base a dove si stà guardando.\ + /// Calcola e modifica la posizione in base a dove si stà guardando.\ + /// Il valore ritornato sarà la posizione modificata che è stata passata in input.\ /// La posizione viene modificata come se si stesse avanzando di una /// unità di spazio.\ /// Es. (0,0) Up -> aumento la y di uno (0,1) - pub fn move_from(&self, pos: Position) -> Position { + pub fn move_from<'a>(&self, pos: &'a mut Position) -> &'a mut Position { match *self { - Direction::Up => Position(pos.0, pos.1 + 1), - Direction::Down => Position(pos.0, pos.1 - 1), - Direction::Right => Position(pos.0 + 1, pos.1), - Direction::Left => Position(pos.0 - 1, pos.1), - Direction::None => pos, - } + Direction::Up => pos.1 += 1, + Direction::Down => pos.1 -= if pos.1 == 0 { 0 } else { 1 }, + Direction::Right => pos.0 += 1, + Direction::Left => pos.0 -= if pos.0 == 0 { 0 } else { 1 }, + Direction::None => (), + }; + pos } /// Restituisce una direzione casuale a partire da un generatore.\ /// La direzione viene generata con una distribuzione uniforme, ovvero non @@ -101,12 +103,7 @@ impl Entity { /// il decisore che permette di muoversi (giocatore o IA) e il piano in cui si trova.\ /// La posizione sarà all'entrata del piano (o in una cella vicina nel caso ci siano altre entità sopra), /// non avrà effetti, azioni o una direzione in particolare. - pub fn new( - name: String, - health: i32, - attack: i32, - behavior: Box, - ) -> Self { + pub fn new(name: String, health: i32, attack: i32, behavior: Box) -> Self { Self { name, behavior, @@ -199,7 +196,7 @@ impl Display for Entity { let times = 20; let health_bar = (self.health * times) / self.health_max; - let filled = "█".repeat(health_bar as usize); + let filled = "■".repeat(health_bar as usize); let empty = " ".repeat((times - health_bar) as usize); let health_bar = format!("[{}{}]", filled, empty); @@ -231,11 +228,10 @@ impl Action { match self { Action::DoNothing => {} Action::Move(direction) => { - let pos = direction.move_from(entity.position); + direction.move_from(&mut entity.position); entity.direction = direction; - entity.position = pos; - let cell = floor.get_cell(pos); + let cell = floor.get_cell(&mut entity.position); cell.entity_over(entity); } } diff --git a/src/es03_game/floor.rs b/src/es03_game/floor.rs index 50df6ed..50daced 100644 --- a/src/es03_game/floor.rs +++ b/src/es03_game/floor.rs @@ -55,8 +55,14 @@ impl Floor { } /// Restituisce la cella nella posizione indicata.\ - /// Con essa si può fare cio che si vuole, e quindi anche modificarla. - pub fn get_cell(&mut self, pos: Position) -> &mut Cell { + /// Con essa si può fare cio che si vuole, e quindi anche 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(&mut self, pos: &mut Position) -> &mut Cell { + let len = self.grid.len() - 1; + pos.0 = pos.0.min(len); + pos.1 = pos.1.min(len); &mut self.grid[pos.0][pos.1] } @@ -102,7 +108,7 @@ impl Floor { } } - /// Crea una view del piano.\ + /// Crea una view del piano con l'entità partecipante all'update. pub fn get_limited_view_floor<'a>(&'a self, entity: &'a Entity) -> FloorView<'a> { FloorView::new(self, entity) } @@ -131,35 +137,26 @@ impl<'a> FloorView<'a> { pub fn as_char_grid(&self) -> Vec> { let grid = &self.floor.grid; let size = grid.len(); - let mut grid: Vec> = (0..size) + let mut grid = (0..size) .map(|y| { - let row = (0..size).map(|x| Some(&grid[x][y])); - let mut row: Vec<_> = row - .clone() - .zip(row.skip(1).chain(std::iter::once(None))) - .flat_map(Self::increase_x_dimension) - .collect(); - row.push('\n'); - row + (0..size) + .flat_map(|x| { + let cell = &grid[x][y]; + let ch = cell.as_char(); + match cell { + Cell::Wall => [ch, ch, ch], + _ => [' ', ch, ' '], + } + }) + .chain(std::iter::once('\n')) + .collect::>() }) - .collect(); + .collect::>(); let pos = &self.entity.position; - grid[pos.1][pos.0 * 2] = self.entity.direction.as_char(); + grid[pos.1][pos.0 * 3 + 1] = self.entity.direction.as_char(); grid } - - fn increase_x_dimension(tuple: (Option<&Cell>, Option<&Cell>)) -> Vec { - let (a, b) = tuple; - 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()] - } - } } impl<'a> Display for FloorView<'a> { diff --git a/src/es03_game/generator.rs b/src/es03_game/generator.rs index 31d494e..f07ea9f 100644 --- a/src/es03_game/generator.rs +++ b/src/es03_game/generator.rs @@ -1,11 +1,21 @@ use super::{ cell::Cell, - config::{Config, ConfigEffect, ConfigEntity}, + config::Config, + entities::{ + Direction::{self, Down, Left, Right, Up}, + Position, + }, floor::Floor, }; use rand::{Rng, SeedableRng}; use rand_pcg::Pcg32; -use std::ops::Range; +use std::{ + collections::{HashMap, HashSet, VecDeque}, + fmt::Display, + ops::Range, +}; + +// todo!() enemies vec configuration? /// Generatore del gioco che può creare dei piani del dungeon. /// Idealmente questo generatore si comporta come il pattern Factory. @@ -13,106 +23,76 @@ use std::ops::Range; /// verrà utilizzato poi dal piano stesso per eventuali altri calcoli. /// Inoltre ad esso viene passato una struttura di config che permette /// di scegliere meglio come poter generare il piano. -pub struct Generator { +pub struct Generator<'a> { pub rng: Pcg32, pub level: usize, - size_rooms: Range, - effects_total: usize, - effects: Vec, - entities_total: usize, - entities: Vec, + config: &'a Config, size: usize, - grid: Vec>, // enemies vec configuration? } -impl Generator { - /// Costruttore standard di un generatore - /// il generatore creato avrà come entità quelle registrate nella configurazione - pub fn new(floor_seed: u64, floor_level: usize, config: &Config) -> Self { +impl<'a> Generator<'a> { + /// Costruttore standard di un generatore, esso avrà tutte le caratteristiche indicate nella configurazione + pub fn new(floor_seed: u64, floor_level: usize, config: &'a Config) -> Self { + let min_floor = config.maze_generation.floor_size.clone().next().unwrap(); + let max_room = config.maze_generation.room_size.clone().last().unwrap(); + assert!(min_floor > max_room, "Floor size should be > than room"); + let mut rand_pcg = Pcg32::seed_from_u64(floor_seed); - let floor_size = rand_pcg.gen_range(config.floor_size.clone()); + let mut floor_size = rand_pcg.gen_range(config.maze_generation.floor_size.clone()); + if floor_size % 2 == 0 { + floor_size = floor_size.max(2) - 1 + } + Self { rng: rand_pcg, level: floor_level, 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)), - entities_total: config.entities_total, - entities: Self::vec_filter(&config.entities, |val| val.floors.contains(&floor_level)), - grid: Self::grid_with_only(floor_size, Cell::Wall), + config, } } - - /// Questo metodo è il più semplice per la generazione del piano.\ - /// Crea una enorme stanza con dei muri attorno e piazza tutti gli effetti. + /// Crea un nuovo labirinto a partire dalle configurazioni passate in input.\ + /// Questo metodo creerà un piano avente delle stanze collegate tra di loro tramite dei + /// corridoi; inoltre in esse verranno inseriti degli effetti. pub fn build_floor(mut self) -> Floor { - for x in 1..(self.size - 1) { - for y in 1..(self.size - 1) { - self.grid[x][y] = Cell::Empty; - } - } + let attempts = self.config.maze_generation.room_placing_attempts; + let room_size = self.config.maze_generation.room_size.clone(); + let mut grid = MazeGenerator::new(self.size, room_size, &mut self.rng) + .generate_rooms(attempts) + .generate_labyrinth(80) + .connect_regions() + .remove_dead_ends(0) + .finalize(Cell::Wall, Cell::Empty); - self.rand_place_cell(Cell::Entance); - self.rand_place_effects(); - Floor::new(self.level, self.rng, vec![], self.grid) - } - - /// TODO - pub fn build_floor_catacomb(mut self) -> Floor { - // init to WALLS - // reserve some cells for rooms ?? - todo!() - } - - /// TODO - fn build_rooms(&mut self) { - todo!() - } - /// crea una stanza in un punto casuale del piano e restituisce il suo boundingbox - fn rand_build_room(&mut self) -> (usize, usize, usize, usize) { - loop { - let (x, y) = self.rand_2d(0..self.size); - let size = self.rand_2d(self.size_rooms.clone()); - let (x_up, y_up) = (x + size.0, y + size.1); - - if x_up < self.size && y_up < self.size { - for x in x..x_up { - for y in y..y_up { - self.grid[x][y] = Cell::Empty - } - } - return (x, y, x_up, y_up); - } - } + self.rand_place(&mut grid, Cell::Entance); + self.rand_place_effects(&mut grid); + Floor::new(self.level, self.rng, vec![], grid) } /// 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) { - for _ in 0..self.effects_total { - let index = self.rng.gen_range(0..self.effects.len()); - let effect = self.effects[index].effect.clone(); + 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)); + + for _ in 0..total { + let index = self.rng.gen_range(0..effects.len()); + let effect = effects[index].effect.clone(); let cell = Cell::Special(effect); - self.rand_place_cell(cell); + self.rand_place(grid, cell); } } /// piazza una cella in un punto casuale del piano.\ /// il metodo contiuna a provare a piazzare la cella finche non trova una cella Empty. - fn rand_place_cell(&mut self, cell: Cell) -> (usize, usize) { + fn rand_place(&mut self, grid: &mut Vec>, cell: Cell) -> (usize, usize) { loop { - let (x, y) = self.rand_2d(0..self.size); - if let Cell::Empty = self.grid[x][y] { - self.grid[x][y] = cell; + let x = self.rng.gen_range(0..self.size.clone()); + let y = self.rng.gen_range(0..self.size); + if let Cell::Empty = grid[x][y] { + grid[x][y] = cell; return (x, y); } } } - /// genera una tupla di due valori randomici nel range passato in input - fn rand_2d(&mut self, range: Range) -> (usize, usize) { - let x = self.rng.gen_range(range.clone()); - let y = self.rng.gen_range(range); - (x, y) - } /// 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 @@ -121,8 +101,424 @@ impl Generator { .filter_map(|val| filter(&val).then(|| val)) .collect() } - /// crea un campo con solamente la cella specificata clonata su tutto di esso - fn grid_with_only(size: usize, cell: Cell) -> Vec> { - vec![vec![cell; size]; size] +} + +/// Utile per la generazione del labirinto.\ +/// L'algoritmo per la generazione del labirinto si può trovare ovunque online, ma in generale è:\ +/// - Piazza delle stanze a caso nella zona.\ +/// - Riempi tutto il resto con un labirinto.\ +/// - Fai dei fori nei vari muri per connettere le stanze e il labirinto.\ +/// - Rimuovi alcuni dead-end del labirinto e fai dei fori in esso.\ +/// \ +/// La fonte degli algoritmi la si può trovare all'articolo: +/// https://journal.stuffwithstuff.com/2014/12/21/rooms-and-mazes/ +/// E la sua implementazione la si può trovare al link di github: +/// https://github.com/munificent/hauberk/blob/db360d9efa714efb6d937c31953ef849c7394a39/lib/src/content/dungeon.dart#L74 +pub struct MazeGenerator<'a> { + size: usize, + rooms_size: Range, + rng: &'a mut Pcg32, + rooms: Vec, + regions: Vec>>, + current_region: usize, +} + +impl Display for MazeGenerator<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let str = (0..self.size) + .into_iter() + .flat_map(|y| { + (0..self.size) + .into_iter() + .map(move |x| { + if let Some(num) = self.regions[x][y] { + format!("{num:2} ") + } else { + "███".to_string() + } + }) + .chain(std::iter::once("\n".to_string())) + }) + .collect::(); + write!(f, "{}", str) + } +} + +impl<'a> MazeGenerator<'a> { + /// Crea un nuovo generatore di stanze a partire dai parametri passati.\ + /// *size* è consigliato che sia un numero dispari, altrimenti alcune zone avranno doppi muri.\ + /// *rooms_size* è consigliato un range come numero maggiore al massimo la metà di size.\ + /// Nota che le stanze generate avranno sempre dimensione dispari per poter generare il labirinto correttamente.\ + /// *rng* indica un generatore di numeri casuali ripetibili, in modo da avere risultati consistenti. + pub fn new(size: usize, rooms_size: Range, rng: &'a mut Pcg32) -> Self { + Self { + size, + rooms_size, + rng, + rooms: vec![], + regions: vec![vec![None; size]; size], + current_region: 0, + } + } + /// Crea il labirinto formato da muri e spazi vuoti passati in input.\ + /// I due parametri passati devono implementare il trait Clone, dato che + /// quando viene creata la matrice, essi verranno messi all'interno di essa. + pub fn finalize(&self, wall: T, empty: T) -> Vec> { + self.regions + .iter() + .map(|col| { + col.iter() + .map(|cell| match cell { + Some(_) => empty.clone(), + None => wall.clone(), + }) + .collect::>() + }) + .collect::>() + } + + /// Rimuove tutti i pezzi di labirinto che non vanno da nessuna parte, o che non sono collegati.\ + /// Per fare ciò cerca tutte le celle vuote che hanno una sola cella vuota collegata. + /// Dopodichè le rimuove e prende quelle rimenenti finchè non ce rimangono più.\ + /// Nel caso si può decidere di lasciare qualche zona che non va a collegarsi da nessuna parte + /// mettendo un numero > 0 nel cutoff.\ + /// Questo indicherà che nel labirinto ci saranno al massimo N corridioi senza uscita. + pub fn remove_dead_ends(mut self, cutoff: usize) -> Self { + let mut dead_ends = (0..self.size) + .into_iter() + .flat_map(|x| { + (0..self.size) + .into_iter() + .map(move |y| Position(x, y)) + .filter(|pos| self.get(pos).is_some()) + .filter(|pos| self.has_near_none(pos, 3)) + }) + .collect::>(); + + while let Some(pos) = dead_ends.pop_front() { + if dead_ends.len() < cutoff { + break; + } + + self.set(&pos, None); + dead_ends.extend( + self.get_near(&pos) + .filter(|pos| self.get(pos).is_some()) + .filter(|pos| self.has_near_none(pos, 3)), + ); + } + + self + } + /// Permette di connettere tutte le zone in modo da avere un grafo collegato invece che sparso.\ + /// Il labirinto, quando vengono create le stanze, non avrà i corridoi e le stanze collegate.\ + /// Questa funzione serve per fare proprio quello, ovvero il collegamento fra di essi.\ + /// Il labirinto si può vedere come un grafo nel quale ci sono delle regioni (stanze e corridoi) scollegate + /// fra di loro, e l'unico modo per metterle assieme è quello di preare degli archi (rompere i muri).\ + pub fn connect_regions(mut self) -> Self { + let mut connectors = self.get_regions_connectors(); + let mut merged = MergeSets::new(1, self.current_region); + let mut keys = connectors.keys().map(|pos| pos.clone()).collect::>(); + keys.sort(); // for repeatability + + while !merged.has_only_one() { + let rand_index = self.rng.gen_range(0..keys.len()); + let pos = &keys[rand_index]; + if let Some(regions) = connectors.remove(pos) { + self.set(pos, Some(0)); + self.current_region += 1; + + merged.merge(regions.into_iter()); + + connectors.remove(&Position(pos.0 + 1, pos.1)); + connectors.remove(&Position(pos.0, pos.1 + 1)); + connectors.remove(&Position(pos.0.saturating_sub(1), pos.1)); + connectors.remove(&Position(pos.0, pos.1.saturating_sub(1))); + } + } + self + } + /// Permette di ricevere una mappa che contiene tutte le posizioni None del labirinto che hanno + /// due o più regioni tra le celle vicine.\ + /// Ciò indica che, nel caso in cui vengano messe a Some(_) le regioni adiacenti ora sono collegate formando + /// una sola grande regione, e quindi collegando parti separate del grafo.\ + /// La lista di punti in cui ciò è possibile contiene tutti i punti esterni delle stanze interne del labirinto.\ + /// Questa operazione è l'equivalente dell'operazione dei grafi al link: https://en.wikipedia.org/wiki/Spanning_tree + fn get_regions_connectors(&self) -> HashMap> { + self.rooms + .iter() + .flat_map(|room| room.get_bounding_points()) + .collect::>() + .into_iter() + .filter(|pos| self.get(pos).is_none()) + .filter_map(|pos| { + let regions = self + .get_near(&pos) + .filter_map(|pos| self.get(&pos)) + .collect::>(); + + if regions.len() >= 2 { + Some((pos, regions)) + } else { + None + } + }) + .collect() + } + /// Riempie tutti gli spazi vuoti della griglia con un labirinto.\ + /// Questo algoritmo lascerà spazi con muri tra i vari cammini e non cercherà + /// di connettersi con altri corridoi (per fare ciò esiste il metodo connect_regions).\ + /// Il parametro da passare indica la percentuale (0..=100) di quanto deve continuare ad + /// andare dritto quando crea il labirinto.\ + /// Con percentuali alte si avranno molti corridoi lunghi, con percentuali basse si avranno + /// molte svolte. + pub fn generate_labyrinth(mut self, mut straight_percentage: usize) -> Self { + straight_percentage = straight_percentage.min(100); // cap at 100 + + for x in (1..self.size).step_by(2) { + for y in (1..self.size).step_by(2) { + let pos = Position(x, y); + if self.get(&pos).is_none() && self.has_near_none(&pos, 4) { + self.grow_maze(pos, straight_percentage); + } + } + } + self + } + /// Crea il labirinto nelle zone vuote a partire dalla posizione indicata.\ + /// Questo metodo è messo privato dato che la posizione di partenza deve essere dispari e + /// deve avere tutti e quattro le celle vicine settate a None.\ + /// L'algoritmo utilizzato è un backtracking iterativo modificato in modo da generare corridioi + /// un pochino più lunghi e lo si può trovare sulla pagina:\ + /// https://en.wikipedia.org/wiki/Maze_generation_algorithm#Iterative_implementation_(with_stack)\ + /// Il parametro straight_percentage indica quanto "scava" i corridoi del labirinto + /// senza girare, e quindi creando lunghi segmenti. + fn grow_maze(&mut self, start: Position, straight_percentage: usize) { + self.current_region += 1; + self.set(&start, Some(self.current_region)); + + let mut prev_direction = Direction::None; + let mut cells = vec![]; + cells.push(start); + + while let Some(mut pos) = cells.pop() { + let directions = self.get_empty_cells_directions(&pos); + prev_direction = if !directions.is_empty() { + // Based on how "windy" passages are, try to prefer carving in the same direction. + let same_direction = self.rng.gen_range(0..=100) < straight_percentage; + let current = if directions.contains(&prev_direction) && same_direction { + prev_direction + } else { + let rand = self.rng.gen_range(0..directions.len()); + directions[rand] + }; + + // save for back-tracking + let prev = pos.clone(); + cells.push(prev); + + // move two times + self.set(current.move_from(&mut pos), Some(self.current_region)); + self.set(current.move_from(&mut pos), Some(self.current_region)); + cells.push(pos); + current + } else { + Direction::None + } + } + } + /// Ritorna tutte le direzioni da cui ci si può spostare da una cella.\ + /// Questo metodo controlla che dalla posizione *pos* si possa andare in una direzione + /// almeno per due passi. In caso positivo, la direzione viene inserita nel risultato.\ + /// Questo metodo viene usato esclusivamente da grow_maze + fn get_empty_cells_directions(&self, pos: &Position) -> Vec { + [Up, Left, Down, Right] + .into_iter() + .filter(|dir| { + let mut pos = pos.clone(); + dir.move_from(&mut pos); + dir.move_from(&mut pos); + pos.0 < self.size && pos.1 < self.size && self.has_near_none(&pos, 4) + }) + .collect() + } + /// Aggiunge delle stanze in modo casuale all'interno della rappresentazione del labirinto.\ + /// Questo metodo non controlla altro che le stanze già inserite per evitare di avere collisioni fra di esse.\ + /// Nel caso in cui questo metodo venga chiamato dopo la generazione del labirinto, e le stanze venissero + /// inserite senza collisioni con quelle precedenti, il labirinto sottostante sarebbe sovrascritto.\ + /// Il parametro attempts indica dopo quanti inserimenti falliti si deve fermare. + pub fn generate_rooms(mut self, mut attempts: u32) -> Self { + while attempts > 0 { + let room = Room::rand(self.rng, self.size, self.rooms_size.clone()); + if self.rooms.iter().any(|other| room.collide(other)) { + attempts -= 1; + } else { + self.current_region += 1; + room.get_area_points() + .for_each(|p| self.set(&p, Some(self.current_region))); + self.rooms.push(room); + } + } + self + } + /// Ritorna un iteratore di posizioni vicine alla posizione indicata.\ + /// Viene ritornato un iteratore in modo che si possa decidere cosa farlo diventare.\ + /// Nel caso una posizione sia fuori dal campo, essa viene scartata e non + /// sarà compresa al'interno dell'iterazione. + fn get_near(&'a self, pos: &'a Position) -> impl Iterator + 'a { + [Up, Left, Down, Right] + .into_iter() + .map(|dir| *dir.move_from(&mut pos.clone())) + .filter(|pos| pos.0 < self.size && pos.1 < self.size) + } + /// Indica se alla posizione passata la cella ha un tot dei vicini None.\ + /// Se infatti si passasse a total 2, significa che questo metodo restituirà + /// true solamente se la cella alla posizione pos ha esattamente 2 vicini None. + fn has_near_none(&self, pos: &Position, total: usize) -> bool { + let total = total.min(4); + self.get_near(pos) + .filter(|pos| self.get(pos).is_none()) + .fold(0, |count, _| count + 1) + == total + } + /// Metodo per l'assegnamento di un valore alla posizione indicata.\ + /// Nel caso si voglia mettere un muro, assegnare None, altrimenti inserire Some(region) per + /// indicare a quale regione quella cella appartiene. + fn set(&mut self, pos: &Position, val: Option) { + self.regions[pos.0][pos.1] = val; + } + /// Permette di prendere il valore contenuto nella cella.\ + /// Nel caso None si indica un muro, mentre in Some(region) si indica la regione quella cella appartiene. + fn get(&self, pos: &Position) -> Option { + self.regions[pos.0][pos.1] + } +} + +/// Struttura ausiliaria usata per contenere le posizioni.\ +/// Vengono implementate alcuni metodi comodi per essi, quali la collisione +/// o la generazione dei punti dei lati.\ +/// La stanza viene rappresentata come un rettangolo, la quale area indica l'interno, +/// mentre i lati non hanno dimensione.\ +/// I punti quindi salvati sono il minimo e il massimo di un rettangolo ed indicano il +/// punto più in basso da dove inizia l'area e quello più in alto. +#[derive(Clone, Copy)] +struct Room { + lo: Position, + hi: Position, +} +impl Room { + /// Crea una stanza random a partire da un massimo valore dei punti raggiungibile + /// e un range che indica il minimo e il massimo della grandezza di una stanza. + pub fn rand(rng: &mut impl Rng, max: usize, range: Range) -> Self { + let x = Self::rand_odd(rng, 0..max); + let y = Self::rand_odd(rng, 0..max); + + // removing one since the odd + odd = even => odd-1 + odd = odd + let x_size = Self::rand_odd(rng, range.clone()) - 1; + let y_size = Self::rand_odd(rng, range) - 1; + + let x_bottom = if x < x_size { 1 } else { x - x_size }; + let y_bottom = if y < y_size { 1 } else { y - y_size }; + + let x_top = (x_bottom + x_size).min(max - 2); + let y_top = (y_bottom + y_size).min(max - 2); + + Self { + lo: Position(x_bottom, y_bottom), + hi: Position(x_top, y_top), + } + } + /// Genera tutti i punti di tutti i lati all'esterno della stanza, insomma i punti dei muri.\ + /// Gli unici punti non generati dall'iteratore ritornato sono quelli degli angoli.\ + /// Es. dato lo(1,1) e hi(2,2) => (1,0), (1,3), (2,0), (2,3), (0,1), (3,1), (0,2), (3,2)\ + /// -XXXX-\ + /// -X██X-\ + /// -X██X-\ + /// -XXXX-\ + pub fn get_bounding_points<'a>(&'a self) -> impl Iterator + 'a { + let x_range = self.lo.0..=self.hi.0; + let y_range = self.lo.1..=self.hi.1; + + let lo_x = self.lo.0 - 1; + let lo_y = self.lo.1 - 1; + let hi_x = self.hi.0 + 1; + let hi_y = self.hi.1 + 1; + + let x_range = x_range.flat_map(move |x| vec![Position(x, lo_y), Position(x, hi_y)]); + let y_range = y_range.flat_map(move |y| vec![Position(lo_x, y), Position(hi_x, y)]); + x_range.chain(y_range) + } + /// Genera tutti i punti all'interno del rettangolo indicato dalla stanza.\ + /// I lati si possono vedere come i muri e l'area come l'interno.\ + /// Cosí facendo, i punti sui lati non verranno generati. + pub fn get_area_points<'a>(&'a self) -> impl Iterator + 'a { + (self.lo.0..=self.hi.0).into_iter().flat_map(|x| { + (self.lo.1..=self.hi.1) + .into_iter() + .map(move |y| Position(x, y)) + }) + } + /// Indica se la stanza creata è in collisione con un'altra passata in input.\ + /// Più precisamente una collisione avviene se l'area di una stanza si sovrappone con l'altra. + /// Il codice risultante deriva dal seguente link:\ + /// https://stackoverflow.com/questions/306316/determine-if-two-rectangles-overlap-each-other + pub fn collide(&self, other: &Self) -> bool { + self.lo.0 <= other.hi.0 + && self.hi.0 >= other.lo.0 + && self.lo.1 <= other.hi.1 + && self.hi.1 >= other.lo.1 + } + /// Genera un numero dispari a partire dal range inserito.\ + /// Questo metodo è utile per il piazzamento della stanza in punti dispari in modo che + /// il labirinto si possa mettere tra i vari muri. + fn rand_odd(rng: &mut impl Rng, range: Range) -> usize { + let mut rand = rng.gen_range(range); + if rand % 2 == 0 { + rand = rand.saturating_sub(1).max(1); + } + rand + } +} + +/// Struttura usata per unire due o più regioni in modo veloce.\ +/// Questo codice è un'implementazione grezza e non ottimizzata di algoritmi UnionFind.\ +#[derive(Debug)] +struct MergeSets { + sets: Vec, + current: usize, + start: usize, + len: usize, +} +impl MergeSets { + /// Crea la struttura UnionFind in modo da avere dei sets numerati da start a total.\ + /// In questo modo possono esistere 4 insiemi, ma a partire dal numero 3 => 3,4,5,6 + pub fn new(start: usize, total: usize) -> Self { + Self { + sets: (start..=total).into_iter().collect(), + current: total + 1, + start, + len: total - start, + } + } + /// Indica se tutti i sets sono stati uniti oppure no.\ + /// Infatti se sono tutti uniti allora ritorna true, altrimenti false. + pub fn has_only_one(&self) -> bool { + self.len == self.sets.len() + } + /// Unisce uno o più regioni indicate dall'iteratore.\ + /// Questo metodo ha complessità pari ad O(n). + pub fn merge(&mut self, regions: impl Iterator) { + let regions = regions + .map(|reg| self.sets[reg - self.start]) + .collect::>(); + self.len = self + .sets + .iter_mut() + .filter(|set| regions.contains(set)) + .fold(0, |count, set| { + *set = self.current; + count + 1 + }); + self.current += 1; } }