Maze Genetation
- added maze gen - refactored Generator - added config for maze gen - changed floor display to 3x instead of 2x
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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<usize>,
|
||||
pub floor_size: Range<usize>,
|
||||
pub maze_generation: ConfigMaze,
|
||||
pub effects_total: usize,
|
||||
pub effects: Vec<ConfigEffect>,
|
||||
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<usize>,
|
||||
pub room_size: Range<usize>,
|
||||
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,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<dyn Behavior>,
|
||||
) -> Self {
|
||||
pub fn new(name: String, health: i32, attack: i32, behavior: Box<dyn Behavior>) -> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Vec<char>> {
|
||||
let grid = &self.floor.grid;
|
||||
let size = grid.len();
|
||||
let mut grid: Vec<Vec<char>> = (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::<Vec<_>>()
|
||||
})
|
||||
.collect();
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
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<char> {
|
||||
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> {
|
||||
|
||||
@@ -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<usize>,
|
||||
effects_total: usize,
|
||||
effects: Vec<ConfigEffect>,
|
||||
entities_total: usize,
|
||||
entities: Vec<ConfigEntity>,
|
||||
config: &'a Config,
|
||||
size: usize,
|
||||
grid: Vec<Vec<Cell>>, // 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<Vec<Cell>>) {
|
||||
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<Vec<Cell>>, 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, 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<T: Clone>(original: &Vec<T>, filter: impl Fn(&T) -> bool) -> Vec<T> {
|
||||
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<Cell>> {
|
||||
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<usize>,
|
||||
rng: &'a mut Pcg32,
|
||||
rooms: Vec<Room>,
|
||||
regions: Vec<Vec<Option<usize>>>,
|
||||
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::<String>();
|
||||
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<usize>, 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<T: Clone>(&self, wall: T, empty: T) -> Vec<Vec<T>> {
|
||||
self.regions
|
||||
.iter()
|
||||
.map(|col| {
|
||||
col.iter()
|
||||
.map(|cell| match cell {
|
||||
Some(_) => empty.clone(),
|
||||
None => wall.clone(),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
/// 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::<VecDeque<_>>();
|
||||
|
||||
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::<Vec<_>>();
|
||||
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<Position, HashSet<usize>> {
|
||||
self.rooms
|
||||
.iter()
|
||||
.flat_map(|room| room.get_bounding_points())
|
||||
.collect::<HashSet<_>>()
|
||||
.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::<HashSet<_>>();
|
||||
|
||||
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<Direction> {
|
||||
[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<Item = Position> + '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<usize>) {
|
||||
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<usize> {
|
||||
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<usize>) -> 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<Item = Position> + '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<Item = Position> + '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>) -> 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<usize>,
|
||||
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<Item = usize>) {
|
||||
let regions = regions
|
||||
.map(|reg| self.sets[reg - self.start])
|
||||
.collect::<HashSet<_>>();
|
||||
self.len = self
|
||||
.sets
|
||||
.iter_mut()
|
||||
.filter(|set| regions.contains(set))
|
||||
.fold(0, |count, set| {
|
||||
*set = self.current;
|
||||
count + 1
|
||||
});
|
||||
self.current += 1;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user