Moved game into rogue_lib crate

This commit is contained in:
2024-05-24 11:48:13 +02:00
parent 24d004db88
commit 012d6322e3
10 changed files with 20 additions and 7 deletions

View File

@@ -1,164 +0,0 @@
use super::{
entities::{Action, Direction, Entity},
floor::Floor,
};
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.
/// Nel caso in cui passi sopra una entià esiste un metodo entity_over che
/// gestisce le varie casistiche.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub enum Cell {
Entance,
Exit,
Special(Box<dyn Effect>),
Wall,
Empty,
}
impl Cell {
/// Data una entità che passa sopra questa cella di spazio
/// modifica la posizione e la fa tornare indietro nel caso sia un muro,
/// nel caso di una cella speciale, applica l'effetto all'entità,
/// e i tutti gli altri casi non fa nulla.\
/// Il movimento tra piani tramite Exit e Entrance non è gestito in questa funzione
/// data la complessità di muovere l'entità.
pub fn entity_over(&mut self, entity: &mut Entity) {
match self {
Cell::Special(effect) => {
entity.add_effect(effect.clone());
if !effect.is_persistent() {
*self = Cell::Empty
}
}
Cell::Wall => {
entity.direction.invert();
entity.direction.move_from(&mut entity.position);
}
_ => (),
}
}
/// 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(effect) => effect.as_char(),
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
/// cella di spazio.\
/// Il trait è taggato con typetag in modo che possa essere utilizzato
/// nella serializzazione e deserializzazione di serde.
/// Esso permette di trasformare le implementazioni di Effect 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 Effect:\
/// #\[typetag::serde\]\
/// \
/// In questo modo si possono creare molteplici effetti che implementano
/// questo trait senza il bisogno di avere un Enum con essi
#[typetag::serde(tag = "type")]
pub trait Effect: DynClone + core::fmt::Debug {
/// Indica se l'effetto rimane nel terreno dopo la sua applicazione ad una entità.\
/// Nel caso di true, l'effetto non verrà rimosso dal terreno,
/// eltrimenti la cella dove si trova questo effetto diventerà Empty
fn is_persistent(&self) -> bool;
/// Applica l'effetto ad una entità.\
/// L'effetto può essere di tutto a partire da un danno a qualcosa di più
/// elaborato come una trappola di nemici.\
/// Tramite l'entità si può anche accedere al piano dove si trova per
/// poter modificare eventualmente qualcosa.
fn apply_to(&self, entity: &mut Entity, floor: &mut Floor);
/// Ritorna un carattere che rappresenta l'effetto.
fn as_char(&self) -> char {
'?'
}
}
clone_trait_object!(Effect);
/// Permette di dare un danno istantaneo a qualunque entità ci passi sopra.\
/// Una volta utilizzato verrà rimosso dal piano.\
/// Nel caso in cui il danno sia negativo, l'entità verrà curata
/// (sempre che la sua vita sia un valore positivo e non negativo)
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct InstantDamage(pub i32);
#[typetag::serde]
impl Effect for InstantDamage {
fn is_persistent(&self) -> bool {
false
}
fn apply_to(&self, entity: &mut Entity, _floor: &mut Floor) {
entity.apply_damage(self.0);
}
fn as_char(&self) -> char {
if self.0 <= 0 {
'+'
} else {
'-'
}
}
}
/// Permettere di infliggere lo stato di confuzione ad una entità.\
/// Esso ignora il successivo comando che verrà impartito all'entità
/// con una probabilità del 50% e inserirà un movimento in una direzione casuale.\
/// Come parametro si può passare per quanti turni l'effetto dura
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Confusion(pub u8);
#[typetag::serde]
impl Effect for Confusion {
fn is_persistent(&self) -> bool {
true
}
fn apply_to(&self, entity: &mut Entity, floor: &mut Floor) {
if self.0 > 0 {
let rng = floor.get_rng();
if rng.gen_bool(0.5) {
let random_direction = Direction::random(rng);
entity.buffer = Action::Move(random_direction);
}
entity.add_effect(Box::new(Self(self.0 - 1)));
}
}
}
/// Permette di infliggere un danno nel tempo.\
/// Similmente a InstantDamage, se il danno è negativo allora il personaggio verrà curato,
/// sempre a patto che la sua vita sia un valore positivo.\
/// L'effetto dura un determinato numero di turni.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TurnBasedDamage {
time: u8,
damage: i32,
}
#[typetag::serde]
impl Effect for TurnBasedDamage {
fn is_persistent(&self) -> bool {
false
}
fn apply_to(&self, entity: &mut Entity, _floor: &mut Floor) {
if self.time > 0 {
entity.apply_damage(self.damage);
entity.add_effect(Box::new(Self {
time: self.time - 1,
damage: self.damage,
}));
}
}
}

View File

@@ -1,118 +0,0 @@
use super::{
cell::{Confusion, Effect, InstantDamage},
entities::{Behavior, RandomMovement},
};
use serde::{Deserialize, Serialize};
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 standard.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Config {
pub game_seed: u64,
pub maze_generation: ConfigMaze,
pub effects_total: usize,
pub effects: Vec<ConfigEffect>,
pub entities_total: usize,
pub entities: Vec<ConfigEntity>,
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, Debug, 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.\
/// \
/// Es. effetto A priorità 1 ed effetto B con priorità 2\
/// Se in Config mettiamo 15 effetti per piano, allora avremo
/// in media 10 A e 5 B per ogni piano.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ConfigEffect {
pub floors: Range<usize>,
pub effect: Box<dyn Effect>,
pub priority: u32,
}
/// Valori di base per le statistiche di un giocatore.\
/// Esse verranno utilizzate quando un giocatore verrà creato.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ConfigPlayer {
pub health: i32,
pub attack: i32,
}
/// Una entità che si può trovare in un piano nel dungeon.\
/// La priorità indica quanto verrà spawnata l'entità in media.\
/// \
/// Es. entità A priorità 1 ed entità B con priorità 2\
/// Se in Config mettiamo 15 entità per piano, allora avremo
/// in media 10 A e 5 B per ogni piano.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ConfigEntity {
pub floors: Range<usize>,
pub name: String,
pub behavior: Box<dyn Behavior>,
pub health: i32,
pub attack: i32,
pub priority: u32,
}
impl Default for Config {
fn default() -> Self {
Self {
game_seed: 0,
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)),
floors: 0..255,
priority: 1,
},
ConfigEffect {
effect: Box::new(InstantDamage(-10)),
floors: 0..255,
priority: 1,
},
ConfigEffect {
effect: Box::new(Confusion(10)),
floors: 0..255,
priority: 10,
},
],
effects_total: 45,
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,
},
}
}
}

View File

@@ -1,354 +0,0 @@
use super::{
cell::{Cell, Effect},
floor::{Floor, FloorView},
};
use dyn_clone::{clone_trait_object, DynClone};
use rand::{Rng, SeedableRng};
use rand_pcg::Pcg32;
use serde::{Deserialize, Serialize};
use std::{collections::VecDeque, fmt::Display, 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(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, Hash, Clone, Copy, Default, Debug, Deserialize, Serialize)]
pub enum Direction {
Up,
Down,
Left,
Right,
#[default]
None,
}
impl Direction {
/// Inverte la direzione attuale. (es. dx -> sx)\
/// Questo metodo modifica la direzione inplace.
pub fn invert(&mut self) {
*self = match *self {
Direction::Up => Direction::Down,
Direction::Down => Direction::Up,
Direction::Right => Direction::Left,
Direction::Left => Direction::Right,
_ => Direction::None,
};
}
/// 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<'a>(&self, pos: &'a mut Position) -> &'a mut Position {
match *self {
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
/// c'è una direzione preferita o con più probabilità.
pub fn random(rng: &mut Pcg32) -> Self {
match rng.gen_range(0..5) {
0 => Direction::Up,
1 => Direction::Down,
2 => Direction::Left,
3 => Direction::Right,
_ => Direction::None,
}
}
/// 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 => '■',
}
}
}
impl Display for Direction {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_char())
}
}
/// Rappresenta una entità all'interno del dungeon.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Entity {
name: String,
effects: VecDeque<Box<dyn Effect>>,
behavior: Option<Box<dyn Behavior>>,
pub buffer: Action,
pub position: Position,
pub direction: Direction,
health_max: i32,
health: i32,
attack: i32,
}
impl Entity {
/// Costruttore che crea una nuova entità a partire dal suo nome, vita, danno di attacco,
/// 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 {
Self {
name,
behavior: Some(behavior),
position: Position(0, 0),
attack,
health,
health_max: health,
buffer: Action::DoNothing,
effects: VecDeque::new(),
direction: Direction::None,
}
}
/// Aggiunge l'effetto passato in input all'entità.\
/// Questo non viene calcolato immediatamente, ma solo quando si chiama la
/// funzione update.\
/// È stato fatto in questo modo dato che ci possono essere effetti che ne aggiungono altri
/// e quindi si farebbe una ricorsione infinita.
pub fn add_effect(&mut self, effect: Box<dyn Effect>) {
self.effects.push_back(effect);
}
/// Indica se l'entità è considerata ancora in gioco o meno.\
/// Per far si che l'entità non sia più in gioco bisobna far arrivare la vita a 0.
/// Nota: una entità con vita negativa è considerata "viva"
pub fn is_alive(&self) -> bool {
self.health != 0
}
/// Restituisce il valore della vita dell'entità.\
pub fn get_health(&self) -> i32 {
self.health
}
/// Restituisce il valore della vita massima dell'entità.\
pub fn get_health_max(&self) -> i32 {
self.health_max
}
/// Restituisce il valore del nome dell'entità.\
pub fn get_name(&self) -> &String {
&self.name
}
/// Applica il valore inserito come danno alla vita.\
/// Nel caso in cui il danno sia negativo allora verrà interpretato come cura.\
/// Nel caso in cui la vita sia negativa la logica sarà inversa.\
/// Il danno/cura non potrà comunque superare lo 0 o la vita massima.
pub fn apply_damage(&mut self, damage: i32) {
let health = self.health - damage;
self.health = if self.health_max > 0 {
health.min(self.health_max).max(0)
} else {
health.max(self.health_max).min(0)
};
}
/// 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à 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) -> Option<Self> {
let mut behavior = mem::take(&mut self.behavior).unwrap();
if !self.is_alive() {
return self.die(behavior, floor);
}
behavior.update(floor.get_limited_view_floor(&self));
let action = self.compute_action(&mut behavior, floor);
if action.is_none() {
return None;
}
if !self.is_alive() {
return self.die(behavior, floor);
}
self.compute_effects(floor);
if !self.is_alive() {
return self.die(behavior, floor);
}
self.behavior = Some(behavior);
Some(self)
}
/// metodo usato per la rimozione dell' entità e del suo behavior
fn die(self, mut behavior: Box<dyn Behavior>, floor: &Floor) -> Option<Self> {
let view = floor.get_limited_view_floor(&self);
behavior.on_death(view);
None
}
/// calcola gli effetti e li applica all'entità.
fn compute_effects(&mut self, floor: &mut Floor) {
let total = self.effects.len(); // len could change
for _ in 0..total {
if let Some(effect) = self.effects.pop_front() {
effect.apply_to(self, floor);
}
}
}
/// prende una decisione e applica l'azione da fare
/// L'azione compiuta viene restituita, altrimenti None
fn compute_action(&mut self, behavior: &mut Box<dyn Behavior>, floor: &mut Floor) -> Option<Action> {
let action = behavior.get_next_action()?;
let action = match self.buffer {
Action::DoNothing => action,
_ => mem::replace(&mut self.buffer, Action::DoNothing),
};
let result = Some(action.clone());
action.apply(self, floor);
result
}
}
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
)
}
}
/// 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, Default, Debug, Deserialize, Serialize)]
pub enum Action {
Move(Direction),
//Attack(Direction),
#[default]
DoNothing,
}
impl Action {
/// Applica l'azione all'entità passata.\
/// Dopo la chiamata di questa funzione l'azione non sarà più disponibile.\
/// Per ogni tipo di azione l'entità viene modificata opportunamente.\
/// \
/// Es. Move(Up) sposterà l'entità da una posizione (x,y) -> (x,y+1)\
/// e applicherà qualunque effetto che si trovi sulla cella di destinazione
pub fn apply(self, entity: &mut Entity, floor: &mut Floor) {
match self {
Action::DoNothing => {}
Action::Move(direction) => {
direction.move_from(&mut entity.position);
entity.direction = direction;
let cell = floor.get_cell_mut(&entity.position);
cell.entity_over(entity);
}
}
}
}
/// Questo trait è molto importante per le entità perchè è responsabile del loro comportamento.\
/// Con questo trait si possono creare diversi comportamenti semplicemente implementandolo
/// e utilizzandolo come parametro nella generazione di una entità.\
/// \
/// Il trait è taggato con typetag in modo che possa essere utilizzato
/// nella serializzazione e deserializzazione di serde.
/// 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 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 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.\
/// 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.\
/// 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.\
/// \
/// 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(&mut self) -> Option<Action>;
}
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, Debug, Serialize, Deserialize)]
pub struct Immovable;
#[typetag::serde]
impl Behavior for Immovable {
fn get_next_action(&mut self) -> Option<Action> {
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<Action> {
Some(mem::take(&mut self.action))
}
}

View File

@@ -1,264 +0,0 @@
use super::{
cell::Cell,
entities::{Entity, Position},
};
use rand_pcg::Pcg32;
use serde::{Deserialize, Serialize};
use std::{
collections::{HashMap, VecDeque},
fmt::Display,
};
/// Indica un piano del dungeon, in essa si possono trovare le celle in cui si
/// cammina e le entità che abitano il piano.\
/// Per poter accedere a questa struttura è necessario utilizzare FloorPtr e fare get()
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Floor {
level: usize,
grid: Vec<Vec<Cell>>,
players: VecDeque<Entity>,
entities: VecDeque<Entity>,
rng: Pcg32,
}
impl Floor {
/// Crea un nuovo piano al livello indicato.\
/// 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<Entity>, grid: Vec<Vec<Cell>>) -> Self {
Self {
level,
rng,
players: VecDeque::new(),
entities: VecDeque::from(entities),
grid,
}
}
/// Aggiunge un giocatore al piano e lo inserisce all'entrata.
pub fn add_player(&mut self, mut player: Entity) {
// todo!() check collision with other entities
player.position = self.get_entrance();
self.players.push_back(player);
}
/// Indica se il piano ha almeno un giocatore in vita o meno
pub fn has_players(&self) -> bool {
self.players.iter().any(|player| player.is_alive())
}
/// Restituisce la grandezza di un lato del piano.\
/// Per avere la quantità di celle basterà prendere il valore ed elevarlo a 2.
pub fn get_size(&self) -> usize {
self.grid.len()
}
/// Restituisce il livello di profondità del piano
pub fn get_level(&self) -> usize {
self.level
}
/// Restituisce il generatore di numeri casuali utilizzato per qualunque cosa
/// inerente al piano (generazione di entità, applicazione di effetti...)
pub fn get_rng(&mut self) -> &mut Pcg32 {
&mut self.rng
}
/// Restituisce la cella nella posizione indicata.\
/// 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(&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 {
self.grid
.iter()
.enumerate()
.find_map(|(x, vec)| {
vec.iter().enumerate().find_map(|(y, cell)| {
if let Cell::Entance = cell {
Some(Position(x, y))
} else {
None
}
})
})
.expect("Entrance of the floor should be inside the grid!")
}
/// 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<Entity> {
let index = self
.players
.iter()
.enumerate()
.filter_map(|(i, player)| {
let pos = &player.position;
match &self.grid[pos.0][pos.1] {
Cell::Exit => Some(i),
_ => None,
}
})
.next();
if let Some(i) = index {
self.players.remove(i)
} else {
None
}
}
/// 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 entity = self.entities.pop_front().unwrap();
if let Some(entity) = entity.update(self) {
self.entities.push_back(entity);
}
}
}
/// 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)
}
/// Ritorna un iteratore a tutte le entità del piano.\
/// Le entità del piano si dividono in giocatori e entità, e questo iteratore le ritorna tutte,
/// passando prima dai giocatori e poi da tutto il resto.
pub fn get_all_entities<'a>(&'a self) -> impl Iterator<Item = &Entity> + 'a {
self.players.iter().chain(self.entities.iter())
}
/// Controlla che nella posizione indicata non ci siano altre entità e restituisce il numero di collisioni trovate.\
/// Questo metodo controlla TUTTE le entità e i giocatori del piano, quindi si svolge in O(n)
fn collisions(&self, pos: &Position) -> usize {
self.get_all_entities()
.filter(|entity| entity.position == *pos)
.fold(0, |count, _| count + 1)
}
}
/// 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<'a> {
pub entity: &'a Entity,
pub floor: &'a Floor,
}
/// todo!() add docs
pub struct CellView<'a> {
pub position: Position,
pub entity: Option<&'a Entity>,
pub cell: &'a Cell,
}
impl<'a> FloorView<'a> {
/// 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(floor: &'a Floor, entity: &'a Entity) -> Self {
Self {
entity: &entity,
floor: &floor,
}
}
/// Ritorna un iteratore contenente gli iteratori di ogni riga del piano.
pub fn get_grid(&self, view: usize) -> impl Iterator<Item = impl Iterator<Item = CellView>> {
let grid = &self.floor.grid;
let entities = self
.floor
.get_all_entities()
.chain(std::iter::once(self.entity))
.map(|entity| (&entity.position, entity))
.collect::<HashMap<_, _>>();
let entities = std::rc::Rc::new(std::cell::RefCell::new(entities));
let temp_x = self.entity.position.0.saturating_sub(view);
let temp_y = self.entity.position.1.saturating_sub(view);
let size_x = temp_x.saturating_add(2 * view).min(grid.len());
let size_y = temp_y.saturating_add(2 * view).min(grid.len());
let view_x = size_x.saturating_sub(2 * view);
let view_y = size_y.saturating_sub(2 * view);
(view_y..size_y).rev().map(move |y| {
let entities = entities.clone();
(view_x..size_x)
.map(move |x| Position(x, y))
.map(move |position| {
let cell = &grid[position.0][position.1];
let entity = entities.borrow_mut().remove(&position);
CellView {
position,
entity,
cell,
}
})
})
}
/// Rappresentazione del piano come matrice di char
pub fn as_char_grid(&self) -> Vec<Vec<char>> {
self.get_grid(self.floor.grid.len())
.map(|iter| {
iter.flat_map(|view| {
if let Some(e) = view.entity {
return [' ', e.direction.as_char(), ' '];
}
let ch = view.cell.as_char();
match view.cell {
Cell::Wall => [ch, ch, ch],
_ => [' ', ch, ' '],
}
})
.chain(std::iter::once('\n'))
.collect::<Vec<_>>()
})
.collect::<Vec<_>>()
}
}
impl<'a> Display for FloorView<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let grid: String = self
.as_char_grid()
.iter()
.map(|row| row.iter().collect::<String>())
.collect();
write!(f, "{}\n{}", grid, self.entity)
}
}

View File

@@ -1,122 +0,0 @@
use super::{
config::Config,
entities::{Behavior, Entity},
floor::Floor,
generator::Generator,
};
use rand::{RngCore, SeedableRng};
use rand_pcg::Pcg32;
use serde::{Deserialize, Serialize};
use std::{
fs::File,
io::{self, BufReader, BufWriter},
};
/// Rappresenta un Dungeon in stile RogueLike.\
/// In esso possiamo trovare dei piani generati casualmente
/// e dei giocatori che esplorano.
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Dungeon {
floors: Vec<Floor>,
config: Config,
rng: Pcg32,
}
impl Dungeon {
/// Crea una nuova istanza di un dungeon con le configurazioni di default
pub fn new() -> Self {
Self::new_with(Config::default())
}
/// Crea una nuova istanza di un dungeon con le configurazioni passate in input
pub fn new_with(config: Config) -> Self {
let mut game = Self {
rng: Pcg32::seed_from_u64(config.game_seed),
floors: vec![],
config,
};
game.build_next_floor();
game
}
/// Carica il dungeon da un file.\
/// Il file deve essere formattato tramite json, altrimenti viene ritornato un errore.
pub fn load(filename: &str) -> io::Result<Self> {
let file = File::open(filename)?;
let reader = BufReader::new(file);
let dungeon: Self = serde_json::from_reader(reader)?;
Ok(dungeon)
}
/// Salva il dungeon corrente nel file indicato.\
/// Il salvataggio viene fatto tramite serializzazione JSON in modo che sia facile da vedere.\
/// Nel caso in cui ci siano problemi con I/O, viene ritornato un errore.
pub fn save(&mut self, filename: &str) -> io::Result<()> {
let file = File::create(filename)?;
let writer = BufWriter::new(file);
let _ = serde_json::to_writer_pretty(writer, self)?;
Ok(())
}
/// 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, decider: Box<dyn Behavior>) {
let stats = &self.config.player_stats;
let player = Entity::new(name, stats.health, stats.attack, decider);
self.floors[0].add_player(player);
}
/// 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(&mut self) -> bool {
self.floors.iter().any(|floor| floor.has_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) -> &Floor {
let floors = self.floors.len() - 1;
let index = level.min(floors);
&self.floors[index]
}
/// Funzione principale del dungeon.\
/// In essa viene fatto fare l'update ai giocatori e ad ogni piano.
/// In generale l'algoritmo è il seguente per ogni piano in cui si trova un giocatore:\
/// - I giocatori fanno le loro mosse.\
/// - Se un giocatore non è più in vita o non può indicare l'azione da fare, viene rimosso
/// - Update di tutte le entità del piano
/// - Modifica di piano di eventuali giocatori
pub fn compute_turn(&mut self) {
let moved = self.floors.iter_mut().fold(None, |moved, floor| {
if floor.has_players() {
let _ = floor.update_players(); //todo!() evantually return the dead players? idk
floor.update_entities();
}
if let Some(player) = moved {
floor.add_player(player);
}
floor.get_player_at_exit()
});
if let Some(player) = moved {
self.build_next_floor();
let len = self.floors.len();
let floor = &mut self.floors[len - 1];
floor.add_player(player);
}
}
/// permette di costruire il piano successivo
fn build_next_floor(&mut self) {
let floor_seed = self.rng.next_u64();
let floor_level = self.floors.len();
let generator = Generator::new(floor_seed, floor_level, &self.config);
let floor = generator.build_floor();
self.floors.push(floor);
}
}

View File

@@ -1,567 +0,0 @@
use super::{
cell::Cell,
config::Config,
entities::{
Direction::{self, Down, Left, Right, Up},
Position,
},
floor::Floor,
};
use rand::{Rng, SeedableRng};
use rand_pcg::Pcg32;
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.
/// Per far si che funzioni ha bisongo di un seed per la generazione del piano
/// 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<'a> {
pub rng: Pcg32,
pub level: usize,
config: &'a Config,
size: usize,
}
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 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,
config,
}
}
/// 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 {
let maze_gen = &self.config.maze_generation;
let room_size = self.config.maze_generation.room_size.clone();
let mut gen = MazeGenerator::new(self.size, room_size, &mut self.rng);
let mut grid = gen
.generate_rooms(maze_gen.room_placing_attempts)
.generate_labyrinth(maze_gen.straight_percentage)
.connect_regions()
.remove_dead_ends(maze_gen.dead_ends)
.finalize(Cell::Wall, Cell::Empty);
let index = gen.get_random_room_index();
let entrance = gen.get_room_ranges(index);
let index = gen.get_random_room_index();
let exit = gen.get_room_ranges(index);
self.rand_place(&mut grid, Cell::Entance, entrance.0, entrance.1);
self.rand_place(&mut grid, Cell::Exit, exit.0, exit.1);
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<Vec<Cell>>) {
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<Vec<Cell>>) {
let effects = vec_filter(&self.config.effects, |e| {
e.floors.contains(&self.level).then(|| (e.priority, e))
});
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);
}
}
/// 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(
&mut self,
grid: &mut Vec<Vec<Cell>>,
cell: Cell,
range_x: Range<usize>,
range_y: Range<usize>,
) -> Position {
loop {
let x = self.rng.gen_range(range_x.clone());
let y = self.rng.gen_range(range_y.clone());
if let Cell::Empty = grid[x][y] {
grid[x][y] = cell;
return Position(x, y);
}
}
}
}
/// crea una vista del vettore passato in input dopo aver applicato la funzione di filtro
pub fn vec_filter<T, F>(original: &Vec<T>, filter: F) -> Vec<(f32, &T)>
where
F: FnMut(&T) -> Option<(u32, &T)>,
{
let temp = original.iter().filter_map(filter).collect::<Vec<_>>();
let max = temp.iter().fold(0, |a, b| a.max(b.0)) + 1;
let total = temp.iter().map(|(p, _)| (max - *p) as f32).sum::<f32>();
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.\
/// 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: u32) -> &mut 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 as usize {
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) -> &mut 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: u32) -> &mut 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: u32) {
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) -> &mut 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]
}
/// Ritorna un indice a caso fra quelli possibili riguardo le stanze create.
pub fn get_random_room_index(&mut self) -> usize {
self.rng.gen_range(0..self.rooms.len())
}
/// Ritorna una coppia di ranges che indicano la zona in cui si trova la stanza indicata fra quelle generate.
pub fn get_room_ranges(&self, index: usize) -> (Range<usize>, Range<usize>) {
let room = &self.rooms[index.min(self.rooms.len())];
let x = room.lo.0..(room.hi.0 + 1);
let y = room.lo.1..(room.hi.1 + 1);
(x, y)
}
}
/// 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, Debug)]
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;
}
}

View File

@@ -1,184 +0,0 @@
use self::{
cell::Cell,
config::Config,
entities::{Action, Behavior, Direction, Entity},
floor::FloorView,
game::Dungeon,
};
use serde::{Deserialize, Serialize};
use std::io::Write;
pub mod cell;
pub mod config;
pub mod entities;
pub mod floor;
pub mod game;
pub mod generator;
/** Es.3
* Implementare una libreria che permetta di realizzare il seguente gioco.
* Il Campo di gioco e' una matrice n x n di celle le celle sui 4 lati sono dei muri e all'interno le celle possono essere
* - vuote
* - contenere cibo (un intero positivo)
* - contenere un veleno (un intero positivo)
*
* Un Giocatore si muove in questa matrice iniziando da una posizione casuale. Il giocatore ha
* - Direzione in cui si muove: Su, Giu', Destra, Sinistra
* - Posizione nella matrice
* - una forza (un intero positivo)
*
* Quando si muove avanza di una posizione nella direzione in cui il giocatore si muove. Una Configurazione e'
* un campo di gioco, e un giocatore in una posizione del campo per questa struttura implementate il trait Display
*
* Il gioco inizia con una configurazione in cui nella matrice ci sono m caselle con cibo e m con veleno (in posizioni casuali), un giocatore in una cella libera e un numero massimo di mosse.
* Ad ogni iterazione: Si lancia una moneta (Testa o Croce) se
* - Testa il giocatore si muove di una posizione nella direzione in cui si sta muovendo
* - altrimenti sceglie casualmente una dell 4 direzioni e fa un passo in quella direzione.
*
* Se la cella in cui si finisce
* contiene cibo, si aggiunge la quantita' di cibo alla forza
* contiene veleno, si decrementa la quantita' di veleno dalla forza
* e' un muro il giocatore rimbalza, cioe' resta nella stessa posizione ma cambia la sua direzione nella direzione opposta.
*
* Il gioco finisce quando
* - il giocatore finisce la forza (cioe' questa diventerebbe un valore <=0) e in questo caso PERDE
* - raggiunge il numero massimo di mosse nel qual caso VINCE
*
* Per n, m, le quantità iniziali dei vari elementi (elemento, cibo, forza) e il numero massimo di mosse usate variabili che possano essere inserite dall'utente.
* Se volete potete anche cambiare le regole del gioco.
* Mettere main e definizioni in files separati (le definizioni in uno o più files) e scrivete i test in una directory a parte.
*/
pub fn run_console(player: String, seed: u64) {
let mut config = Config::default();
config.game_seed = seed;
let mut game = Dungeon::new_with(config);
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<Item = String>,
) -> impl Iterator<Item = String> {
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";
const COLOR_PLAYER: &str = "\x1b[38;5;166m";
const COLOR_PLAYER_HEALTH: &str = "\x1b[31m";
/// Implementazione di una possibile interfaccia console.\
/// Ha fin troppi metodi per far vedere in modo carino il gioco, ma comunque la parte importante
/// è l'implementazione del Behavior.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ConsoleInput;
impl ConsoleInput {
/// todo!() add docs
fn print_floor(&self, floor: FloorView, other: String) {
let mut term = console::Term::stdout();
let _ = term.clear_screen();
let _ = term.write_fmt(format_args!(
"{}{}\n{other}\n",
Self::floor_as_string(&floor),
Self::entity_as_string(floor.entity),
));
}
/// todo!() add docs
fn entity_as_string(entity: &Entity) -> String {
let times = 20;
let health_bar = (entity.get_health() * times) / entity.get_health_max();
let filled = "".repeat(health_bar as usize);
let empty = " ".repeat((times - health_bar) as usize);
format!(
"{}: [{COLOR_PLAYER_HEALTH}{filled}{empty}{COLOR_RESET}] {:4}/{:4}",
entity.get_name(),
entity.get_health(),
entity.get_health_max()
)
}
/// todo!() add docs
fn floor_as_string(floor: &FloorView) -> String {
let view = 5;
let size = (2 * view) * 3;
let iter = floor.get_grid(view).map(|iter| {
iter.map(|view| {
if let Some(e) = view.entity {
let color = if floor.entity.position == e.position {
COLOR_PLAYER
} else {
COLOR_ENEMY
};
return format!("{} {} {COLOR_RESET}", color, e.direction.as_char());
}
let cell = view.cell.as_char();
match view.cell {
Cell::Special(_) => format!("{COLOR_EFFECT} {cell} {COLOR_RESET}"),
Cell::Wall => format!("{cell}{cell}{cell}"),
_ => format!(" {cell} "),
}
})
.collect()
});
let title = format!(" Floor lv.{:2} ", floor.floor.get_level());
box_of(size, title, iter).collect()
}
}
#[typetag::serde]
impl Behavior for ConsoleInput {
fn update(&mut self, floor: FloorView) {
self.print_floor(floor, "".to_string());
}
fn on_death(&mut self, floor: FloorView) {
self.print_floor(floor, "YOU DIED!".to_string());
}
fn get_next_action(&mut self) -> Option<Action> {
let mut term = console::Term::stdout();
let _ = term.write("Insert your action [wasd or space for nothing]: ".as_bytes());
loop {
if let Ok(ch) = term.read_char() {
match ch {
' ' => 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)),
_ => (),
}
}
}
}
}

View File

@@ -1,6 +1,5 @@
pub mod es01_anagram;
pub mod es02_rational;
pub mod es03_game;
pub mod es04_rational_traits;
pub mod es05_bank;
pub mod es06_list;