diff --git a/src/config.rs b/src/config.rs index 3bfc207..6a3291e 100644 --- a/src/config.rs +++ b/src/config.rs @@ -44,6 +44,9 @@ pub struct GenerationConfig { /// (min, max) distance between platforms pub platform_distance_bounds: (usize, usize), + /// probability for doing the last shift direction again + pub momentum_prob: f32, + // ------- TODO: these should go somewhere else ----- pub waypoints: Vec, } @@ -99,6 +102,7 @@ impl Default for GenerationConfig { ], step_weights: vec![20, 11, 10, 9], platform_distance_bounds: (500, 750), + momentum_prob: 0.01, } } } diff --git a/src/editor.rs b/src/editor.rs index c6498d1..9c4ac98 100644 --- a/src/editor.rs +++ b/src/editor.rs @@ -374,6 +374,14 @@ impl Editor { true, ); + field_edit_widget( + ui, + &mut self.config.momentum_prob, + edit_f32, + "momentum prob", + true, + ); + // only show these in setup mode ui.add_visible_ui(self.is_setup(), |ui| { vec_edit_widget( diff --git a/src/generator.rs b/src/generator.rs index c8f3666..fea83a2 100644 --- a/src/generator.rs +++ b/src/generator.rs @@ -37,18 +37,18 @@ impl Generator { if !self.walker.finished { // randomly mutate kernel - self.walker.mutate_kernel(config, &mut self.rnd); + self.walker.mutate_kernel(&config, &mut self.rnd); // perform one step self.walker - .probabilistic_step(&mut self.map, &mut self.rnd)?; + .probabilistic_step(&mut self.map, config, &mut self.rnd)?; // handle platforms self.walker.check_platform( &mut self.map, config.platform_distance_bounds.0, config.platform_distance_bounds.1, - ); + )?; } Ok(()) @@ -56,7 +56,7 @@ impl Generator { /// Post processing step to fix all existing edge-bugs, as certain inner/outer kernel /// configurations do not ensure a min. 1-block freeze padding consistently. - fn fix_edge_bugs(&mut self) -> Array2 { + fn fix_edge_bugs(&mut self) -> Result, &'static str> { let mut edge_bug = Array2::from_elem((self.map.width, self.map.height), false); let width = self.map.width; let height = self.map.height; @@ -71,8 +71,12 @@ impl Generator { continue; } - let neighbor_x = x + dx - 1; // TODO: deal with overflow? - let neighbor_y = y + dy - 1; + let neighbor_x = (x + dx) + .checked_sub(1) + .ok_or("fix edge bug out of bounds")?; + let neighbor_y = (y + dy) + .checked_sub(1) + .ok_or("fix edge bug out of bounds")?; if neighbor_x < width && neighbor_y < height { let neighbor_value = &self.map.grid[[neighbor_x, neighbor_y]]; if *neighbor_value == BlockType::Hookable { @@ -90,15 +94,17 @@ impl Generator { } } - edge_bug + Ok(edge_bug) } pub fn post_processing(&mut self) { - self.fix_edge_bugs(); + self.fix_edge_bugs().expect("fix edge bugs failed"); self.map - .generate_room(&self.map.spawn.clone(), 4, Some(&BlockType::Start)); + .generate_room(&self.map.spawn.clone(), 2, Some(&BlockType::Start)) + .expect("start room generation failed"); self.map - .generate_room(&self.walker.pos.clone(), 4, Some(&BlockType::Finish)); + .generate_room(&self.walker.pos.clone(), 2, Some(&BlockType::Finish)) + .expect("start finish room generation"); } /// Generates an entire map with a single function call. This function is used by the CLI. diff --git a/src/map.rs b/src/map.rs index 600c99f..069b7b2 100644 --- a/src/map.rs +++ b/src/map.rs @@ -130,23 +130,35 @@ impl Map { Ok(()) } - pub fn generate_room(&mut self, pos: &Position, margin: usize, zone_type: Option<&BlockType>) { - // TODO: ensure valid position? + pub fn generate_room( + &mut self, + pos: &Position, + margin: usize, + zone_type: Option<&BlockType>, + ) -> Result<(), &'static str> { + // TODO: return an error? + if pos.x < (margin + 1) + || pos.y < (margin + 1) + || pos.x > self.width - (margin + 1) + || pos.y > self.height - (margin + 1) + { + return Err("generate room out of bounds"); + } let margin: i32 = margin.to_i32().unwrap(); // carve room self.set_area( - &pos.shifted_by(-margin, -margin), - &pos.shifted_by(margin, margin), + &pos.shifted_by(-margin, -margin)?, + &pos.shifted_by(margin, margin)?, &BlockType::Empty, true, ); // set platform self.set_area( - &pos.shifted_by(-(margin - 2), 1), - &pos.shifted_by(margin - 2, 1), + &pos.shifted_by(-(margin - 2), 1)?, + &pos.shifted_by(margin - 2, 1)?, &BlockType::Platform, true, ); @@ -154,8 +166,8 @@ impl Map { // set spawns if zone_type == Some(&BlockType::Start) { self.set_area( - &pos.shifted_by(-(margin - 2), 0), - &pos.shifted_by(margin - 2, 0), + &pos.shifted_by(-(margin - 2), 0)?, + &pos.shifted_by(margin - 2, 0)?, &BlockType::Spawn, true, ); @@ -163,12 +175,14 @@ impl Map { // set start/finish line if let Some(zone_type) = zone_type { self.set_area_border( - &pos.shifted_by(-margin - 1, -margin - 1), - &pos.shifted_by(margin + 1, margin + 1), + &pos.shifted_by(-margin - 1, -margin - 1)?, + &pos.shifted_by(margin + 1, margin + 1)?, zone_type, false, ); } + + Ok(()) } fn pos_to_chunk_pos(&self, pos: Position) -> Position { @@ -211,13 +225,16 @@ impl Map { top_left: &Position, bot_right: &Position, value: &BlockType, - ) -> bool { - let area = self.grid.slice(s![ - top_left.x..=bot_right.x + 1, - top_left.y..=bot_right.y + 1 - ]); + ) -> Result { + if !self.pos_in_bounds(&top_left) || !self.pos_in_bounds(&bot_right) { + return Err("checking area out of bounds"); + } + + let area = self + .grid + .slice(s![top_left.x..=bot_right.x, top_left.y..=bot_right.y]); - area.iter().any(|block| block == value) + Ok(area.iter().any(|block| block == value)) } pub fn check_area_all( @@ -225,13 +242,15 @@ impl Map { top_left: &Position, bot_right: &Position, value: &BlockType, - ) -> bool { - let area = self.grid.slice(s![ - top_left.x..=bot_right.x + 1, - top_left.y..=bot_right.y + 1 - ]); + ) -> Result { + if !self.pos_in_bounds(&top_left) || !self.pos_in_bounds(&bot_right) { + return Err("checking area out of bounds"); + } + let area = self + .grid + .slice(s![top_left.x..=bot_right.x, top_left.y..=bot_right.y]); - area.iter().all(|block| block == value) + Ok(area.iter().all(|block| block == value)) } // TODO: right now override is hardcoded to overide empty AND freeze. i might need some diff --git a/src/position.rs b/src/position.rs index ece825a..f28b3b2 100644 --- a/src/position.rs +++ b/src/position.rs @@ -32,13 +32,13 @@ impl Position { } /// returns a new position shifted by some x and y value - pub fn shifted_by(&self, x_shift: i32, y_shift: i32) -> Position { + pub fn shifted_by(&self, x_shift: i32, y_shift: i32) -> Result { let new_x = match x_shift >= 0 { true => self.x + (x_shift as usize), false => self .x .checked_sub((-x_shift) as usize) - .expect("shift is out of bounds"), + .ok_or("invalid shift")?, }; let new_y = match y_shift >= 0 { @@ -46,10 +46,10 @@ impl Position { false => self .y .checked_sub((-y_shift) as usize) - .expect("shift is out of bounds"), + .ok_or("invalid shift")?, }; - Position::new(new_x, new_y) + Ok(Position::new(new_x, new_y)) } pub fn shift_in_direction( diff --git a/src/walker.rs b/src/walker.rs index 41c3ffb..3a90c30 100644 --- a/src/walker.rs +++ b/src/walker.rs @@ -2,7 +2,7 @@ use crate::{ config::GenerationConfig, kernel::Kernel, map::{BlockType, KernelType, Map}, - position::Position, + position::{Position, ShiftDirection}, random::Random, }; @@ -21,6 +21,8 @@ pub struct CuteWalker { pub finished: bool, pub steps_since_platform: usize, + + pub last_direction: Option, } impl CuteWalker { @@ -40,6 +42,7 @@ impl CuteWalker { waypoints: config.waypoints.clone(), finished: false, steps_since_platform: 0, + last_direction: None, } } @@ -59,12 +62,17 @@ impl CuteWalker { /// will try to place a platform at the walkers position. /// If force is true it will enforce a platform. - pub fn check_platform(&mut self, map: &mut Map, min_distance: usize, max_distance: usize) { + pub fn check_platform( + &mut self, + map: &mut Map, + min_distance: usize, + max_distance: usize, + ) -> Result<(), &'static str> { self.steps_since_platform += 1; // Case 1: min distance is not reached -> skip if self.steps_since_platform < min_distance { - return; + return Ok(()); } let walker_pos = self.pos.clone(); @@ -72,31 +80,34 @@ impl CuteWalker { // Case 2: max distance has been exceeded -> force platform using a room if self.steps_since_platform > max_distance { // TODO: for now this is hardcoded so that platform is shifted down by 7 blocks. - map.generate_room(&walker_pos.shifted_by(0, 7), 4, None); + // map.generate_room(&walker_pos.shifted_by(0, 7)?, 4, None)?; self.steps_since_platform = 0; - return; + return Ok(()); } // Case 3: min distance has been exceeded -> Try to place platform, but only if possible let area_empty = map.check_area_all( - &walker_pos.shifted_by(-2, -3), - &walker_pos.shifted_by(2, 1), + &walker_pos.shifted_by(-2, -3)?, + &walker_pos.shifted_by(2, 1)?, &BlockType::Empty, - ); + )?; if area_empty { map.set_area( - &walker_pos.shifted_by(-1, 0), - &walker_pos.shifted_by(1, 0), + &walker_pos.shifted_by(-1, 0)?, + &walker_pos.shifted_by(1, 0)?, &BlockType::Platform, true, ); self.steps_since_platform = 0; } + + Ok(()) } pub fn probabilistic_step( &mut self, map: &mut Map, + config: &GenerationConfig, rnd: &mut Random, ) -> Result<(), &'static str> { if self.finished { @@ -105,11 +116,17 @@ impl CuteWalker { let goal = self.goal.as_ref().ok_or("Error: Goal is None")?; let shifts = self.pos.get_rated_shifts(goal, map); - let sampled_shift = rnd.sample_move(&shifts); + let mut sampled_shift = &rnd.sample_move(&shifts); + + // with a certain probabiliy re-use last direction instead + if rnd.with_probability(config.momentum_prob) && !self.last_direction.is_none() { + sampled_shift = self.last_direction.as_ref().unwrap(); + } // apply that shift - self.pos.shift_in_direction(&sampled_shift, map)?; + self.pos.shift_in_direction(sampled_shift, map)?; self.steps += 1; + self.last_direction = Some(sampled_shift.clone()); // remove blocks using a kernel at current position map.update(self, KernelType::Outer)?;