diff --git a/petri/src/lib.rs b/petri/src/lib.rs index bb8f465..fe9ede4 100644 --- a/petri/src/lib.rs +++ b/petri/src/lib.rs @@ -1,5 +1,3 @@ -use std::ops::Not; - use rand::prelude::*; use serde::{Deserialize, Serialize}; @@ -11,25 +9,10 @@ pub struct Cell(pub u16); #[derive(Debug, Serialize, Deserialize)] pub struct Dish { #[serde(skip)] - world: World, - pub rules: Vec, // todo make read-only to ensure cache is updated + pub chunk: Chunk, + pub rules: Vec, pub types: Vec, - pub groups: Vec, // todo make read-only to ensure cache is updated - #[serde(skip)] - cache: Vec, - #[serde(skip)] - match_cache: Vec, - #[serde(skip)] - max_rule_width: usize, - #[serde(skip)] - max_rule_height: usize, -} - -#[derive(Debug)] -struct RuleCache { - rule: usize, - variant: usize, - matches: Vec<(isize, isize)>, + pub groups: Vec, } #[derive(Debug, Default, Serialize, Deserialize)] @@ -46,15 +29,10 @@ pub struct CellData { } #[derive(Debug, Default)] -struct Chunk { +pub struct Chunk { pub contents: Box<[[Cell; CHUNK_SIZE]; CHUNK_SIZE]>, } -#[derive(Debug, Default)] -struct World { - chunk: Chunk, -} - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Rule { #[serde(default)] @@ -192,22 +170,6 @@ impl Rule { self.base.width } - fn max_width(&self) -> usize { - self.variants - .iter() - .map(|r| r.width) - .max() - .unwrap_or_default() - } - - fn max_height(&self) -> usize { - self.variants - .iter() - .map(|r| r.height) - .max() - .unwrap_or_default() - } - pub fn resize(&mut self, params: ResizeParam) { let (dw, dh, dx, dy) = params; @@ -327,11 +289,7 @@ impl Chunk { } } - fn fill(&mut self, cell: Cell) { - self.contents.fill([cell; CHUNK_SIZE]); - } - - fn with_random_ones(mut self) -> Self { + fn fill_random(mut self) -> Self { for col in self.contents.iter_mut() { for cell in col.iter_mut() { if random::() % 4 == 0 { @@ -342,7 +300,7 @@ impl Chunk { self } - fn get_cell(&self, x: usize, y: usize) -> Cell { + pub fn get_cell(&self, x: usize, y: usize) -> Cell { self.contents[x][y] } @@ -389,10 +347,8 @@ impl Dish { rule.generate_variants() } - let mut new = Self { - world: World { - chunk: Chunk::new().with_random_ones(), - }, + Self { + chunk: Chunk::new().fill_random(), rules: default_rules, types: vec![ CellData::new("air", 0, 0, 0), @@ -403,233 +359,13 @@ impl Dish { void: true, cells: vec![Cell(0)], }], - cache: Vec::new(), - match_cache: Vec::new(), - max_rule_height: 1, - max_rule_width: 1, - }; - new.update_all_rules(); - new + } } - pub fn fill(&mut self, cell: Cell) { - self.world.fill(cell); - self.rebuild_cache(); - } - - pub fn update_all_rules(&mut self) { - self.max_rule_height = 1; - self.max_rule_width = 1; + pub fn update_rules(&mut self) { for rule in &mut self.rules { rule.generate_variants(); - self.max_rule_height = self.max_rule_height.max(rule.max_height()); - self.max_rule_width = self.max_rule_width.max(rule.max_width()); } - self.rebuild_cache(); - } - - /// run after any rule modification - pub fn update_cache_single_rule(&mut self, rule_index: usize) { - // remove old cache for this rule, since the variants may have changed - self.cache.retain(|c| c.rule != rule_index); - self.add_cache_single_rule(rule_index); - self.update_match_cache(); - } - - /// run after adding a rule - pub fn cache_last_added_rule(&mut self) { - if self.rules.is_empty() { - return; - } - let index = self.rules.len() - 1; - self.update_cache_single_rule(index); - } - - fn add_cache_single_rule(&mut self, rule_index: usize) { - let rule = &self.rules[rule_index]; - if !rule.enabled { - return; - } - for variant_index in 0..rule.variants.len() { - let mut matches = Vec::new(); - - let rule = &rule.variants[variant_index]; - let border_x = rule.width as isize - 1; - let border_y = rule.height as isize - 1; - - for px in -border_x..(CHUNK_SIZE as isize) { - for py in -border_y..(CHUNK_SIZE as isize) { - if self.world.subrule_matches(px, py, rule, &self.groups) { - matches.push((px, py)); - } - } - } - self.cache.push(RuleCache { - rule: rule_index, - variant: variant_index, - matches, - }); - } - } - - pub fn rebuild_cache(&mut self) { - println!("rebuilding cache"); - self.cache.clear(); - for rule_index in 0..self.rules.len() { - self.add_cache_single_rule(rule_index); - } - self.update_match_cache(); - } - - pub fn update_cache(&mut self, x: isize, y: isize, width: usize, height: usize) { - fn overlap( - (x1, y1, w1, h1): (isize, isize, usize, usize), - (x2, y2, w2, h2): (isize, isize, usize, usize), - ) -> bool { - x2 < x1.saturating_add_unsigned(w1) - && x1 < x2.saturating_add_unsigned(w2) - && y2 < y1.saturating_add_unsigned(h1) - && y1 < y2.saturating_add_unsigned(h2) - } - let edited_rect = (x, y, width, height); - - for cache in &mut self.cache { - let rule = &self.rules[cache.rule].variants[cache.variant]; - let rule_width = rule.width; - let rule_height = rule.height; - - // discard all overlapping matches - let mut i = 0; - while i < cache.matches.len() { - let match_pos = cache.matches[i]; - let match_rect = (match_pos.0, match_pos.1, rule_width, rule_height); - if overlap(edited_rect, match_rect) { - cache.matches.swap_remove(i); - } else { - i += 1; - } - } - // check entire changed area and add matches - let border_x = rule_width - 1; - let border_y = rule_height - 1; - - for px in (x.wrapping_sub_unsigned(border_x))..(x.wrapping_add_unsigned(width)) { - for py in (y.wrapping_sub_unsigned(border_y))..(y.wrapping_add_unsigned(height)) { - if self.world.subrule_matches(px, py, rule, &self.groups) { - cache.matches.push((px, py)); - } - } - } - } - self.update_match_cache(); - } - - fn update_match_cache(&mut self) { - self.match_cache = self - .cache - .iter() - .enumerate() - .filter_map(|(i, c)| c.matches.is_empty().not().then_some(i)) - .collect(); - } - - /// picks a random match from any rule with at least one match - pub fn apply_one_match(&mut self) { - if self.match_cache.is_empty() { - return; - } - let i = random::() % self.match_cache.len(); - let i = self.match_cache[i]; - let rule_cache = &self.cache[i]; - let match_pos_index = random::() % rule_cache.matches.len(); - let (x, y) = rule_cache.matches[match_pos_index]; - - let rule = &self.rules[rule_cache.rule].variants[rule_cache.variant]; - let width = rule.width; - let height = rule.height; - - self.apply_rule(x, y, rule_cache.rule, rule_cache.variant); - self.update_cache(x, y, width, height); - } - - /// Picks a random point and applies a random match at that position, if any exist. - /// The random point can be outside the world bounds, to catch cases where the root (top-left) of a match is outside the bounds. - /// TODO make sure max_rule_[width/height] is up to date after each rule.generate_variants - pub fn fire_blindly_cached(&mut self) { - let border_x = self.max_rule_width - 1; - let border_y = self.max_rule_height - 1; - let x = ((random::() % (CHUNK_SIZE + border_x)) as isize) - .wrapping_sub_unsigned(border_x); - let y = ((random::() % (CHUNK_SIZE + border_y)) as isize) - .wrapping_sub_unsigned(border_y); - - let matches = self.get_matches_at_point(x, y); - if matches.is_empty() { - return; - } - let i = random::() % matches.len(); - let (rule_index, variant_index) = matches[i]; - self.apply_rule(x, y, rule_index, variant_index); - let rule = &self.rules[rule_index].variants[variant_index]; - let width = rule.width; - let height = rule.height; - self.update_cache(x, y, width, height); - } - - /// Picks a random point and applies a random match that overlaps with it, if any exist. - /// TODO benchmark and only keep one of try_one_position_overlapped and fire_blindly_cached - pub fn try_one_position_overlapped(&mut self) { - let x = (random::() % CHUNK_SIZE) as isize; - let y = (random::() % CHUNK_SIZE) as isize; - - let matches = self.get_matches_containing_point(x, y); - if matches.is_empty() { - return; - } - let i = random::() % matches.len(); - let (x, y, rule_index, variant_index) = matches[i]; - self.apply_rule(x, y, rule_index, variant_index); - let rule = &self.rules[rule_index].variants[variant_index]; - let width = rule.width; - let height = rule.height; - self.update_cache(x, y, width, height); - } - - fn get_matches_at_point(&self, x: isize, y: isize) -> Vec<(usize, usize)> { - self.cache - .iter() - .flat_map(|rule| { - rule.matches.iter().filter_map(|&(mx, my)| { - (mx == x && my == y).then_some((rule.rule, rule.variant)) - }) - }) - .collect() - } - - fn get_matches_containing_point( - &self, - x: isize, - y: isize, - ) -> Vec<(isize, isize, usize, usize)> { - fn contains((x, y, w, h): (isize, isize, usize, usize), (px, py): (isize, isize)) -> bool { - px >= x - && py >= y && px < x.saturating_add_unsigned(w) - && py < y.saturating_add_unsigned(h) - } - self.cache - .iter() - .flat_map(|rule| { - let variant = &self.rules[rule.rule].variants[rule.variant]; - let (w, h) = (variant.width, variant.height); - rule.matches.iter().filter_map(move |&(mx, my)| { - if contains((mx, my, w, h), (x, y)) { - Some((mx, my, rule.rule, rule.variant)) - } else { - None - } - }) - }) - .collect() } pub fn fire_blindly(&mut self) { @@ -645,8 +381,12 @@ impl Dish { if enabled_rules.is_empty() { return; } - let enabled_rule_index = random::() % enabled_rules.len(); - let rule_index = enabled_rules[enabled_rule_index]; + let rule = random::() % enabled_rules.len(); + let rule = enabled_rules[rule]; + self.fire_rule(rule); + } + + fn fire_rule(&mut self, rule_index: usize) { let rule = &self.rules[rule_index]; let variant_index = random::() % rule.variants.len(); let variant = &rule.variants[variant_index].clone(); @@ -657,23 +397,17 @@ impl Dish { let y = ((random::() % (CHUNK_SIZE + border_y)) as isize) .wrapping_sub_unsigned(border_y); - if self.world.subrule_matches(x, y, variant, &self.groups) { - self.apply_rule(x, y, rule_index, variant_index); + if !self.subrule_matches(x, y, variant) { + return; } - } - fn apply_rule(&mut self, x: isize, y: isize, rule_index: usize, variant_index: usize) { - let rule = &self.rules[rule_index]; - let variant = &rule.variants[variant_index].clone(); - - if rule.failrate != 0 && rule.failrate > random() { - // TODO don't update cache after this + let fail: u8 = random(); + if rule.failrate > fail { return; } let width = variant.width; let height = variant.height; - let mut old_state = Vec::new(); for dy in 0..height { for dx in 0..width { @@ -702,7 +436,6 @@ impl Dish { RuleCellTo::Copy(x, y) => { let index = x + y * variant.width; if index >= old_state.len() { - // TODO sanitize the rules somewhere else and remove this bounds check // the copy source is outside the rule bounds continue; } @@ -718,35 +451,7 @@ impl Dish { } } - //todo isize - pub fn get_cell(&self, x: usize, y: usize) -> Option { - self.world.get_cell(x, y) - } - - //todo isize - pub fn set_cell(&mut self, x: usize, y: usize, cell: Cell) { - if x >= CHUNK_SIZE || y >= CHUNK_SIZE { - return; - } - self.world.chunk.set_cell(x, y, cell); - } -} - -impl World { - fn fill(&mut self, cell: Cell) { - self.chunk.fill(cell); - } - - //todo isize - fn get_cell(&self, x: usize, y: usize) -> Option { - if x >= CHUNK_SIZE || y >= CHUNK_SIZE { - None - } else { - Some(self.chunk.get_cell(x, y)) - } - } - - fn subrule_matches(&self, x: isize, y: isize, subrule: &SubRule, groups: &[CellGroup]) -> bool { + fn subrule_matches(&self, x: isize, y: isize, subrule: &SubRule) -> bool { for dx in 0..subrule.width { for dy in 0..subrule.height { let x = x.wrapping_add_unsigned(dx) as usize; @@ -759,7 +464,7 @@ impl World { } } RuleCellFrom::Group(group_id) => { - let group = &groups[group_id]; + let group = &self.groups[group_id]; if let Some(cell) = cell { if !group.cells.contains(&cell) { return false; @@ -774,6 +479,23 @@ impl World { } true } + + //todo isize + pub fn get_cell(&self, x: usize, y: usize) -> Option { + if x >= CHUNK_SIZE || y >= CHUNK_SIZE { + None + } else { + Some(self.chunk.get_cell(x, y)) + } + } + + //todo isize + pub fn set_cell(&mut self, x: usize, y: usize, cell: Cell) { + if x >= CHUNK_SIZE || y >= CHUNK_SIZE { + return; + } + self.chunk.set_cell(x, y, cell) + } } impl Cell { diff --git a/uscope/src/main.rs b/uscope/src/main.rs index 4b66657..6c51aa0 100644 --- a/uscope/src/main.rs +++ b/uscope/src/main.rs @@ -14,7 +14,7 @@ use egui::{collapsing_header::CollapsingState, DragValue, PointerButton}; use native_dialog::FileDialog; use rand::prelude::*; -use petri::{Cell, CellData, CellGroup, Dish, Rule, RuleCellFrom, RuleCellTo, CHUNK_SIZE}; +use petri::{Cell, CellData, CellGroup, Chunk, Dish, Rule, RuleCellFrom, RuleCellTo, CHUNK_SIZE}; fn main() { eframe::run_native( @@ -29,7 +29,7 @@ fn main() { struct UScope { dish: Dish, brush: Cell, - speed: u32, + speed: usize, show_grid: bool, } @@ -37,7 +37,7 @@ impl UScope { fn new(_cc: &eframe::CreationContext<'_>) -> Self { Self { dish: Dish::new(), - speed: 50, + speed: 500, show_grid: false, brush: Cell(1), } @@ -65,7 +65,7 @@ impl UScope { // TODO: show errors to user let s = fs::read_to_string(path).unwrap(); self.dish = serde_json::from_str(&s).unwrap(); - self.dish.update_all_rules(); + self.dish.update_rules(); } } } @@ -74,18 +74,15 @@ impl eframe::App for UScope { fn update(&mut self, ctx: &eframe::egui::Context, _frame: &mut eframe::Frame) { ctx.request_repaint(); for _ in 0..self.speed { - self.dish.try_one_position_overlapped(); + self.dish.fire_blindly(); } SidePanel::left("left_panel") .min_width(100.) .show(ctx, |ui| { ui.heading("Simulation"); ui.label("speed"); - ui.add(Slider::new(&mut self.speed, 0..=500).clamp_to_range(false)); + ui.add(Slider::new(&mut self.speed, 0..=5000).clamp_to_range(false)); ui.checkbox(&mut self.show_grid, "show grid"); - if ui.button("regenerate rules and cache").clicked() { - self.dish.update_all_rules(); - } ui.horizontal(|ui| { if ui.button("Save").clicked() { self.save_universe(); @@ -116,7 +113,7 @@ impl eframe::App for UScope { self.dish.types.push(CellData { name, color }) } if ui.button("fill").clicked() { - self.dish.fill(self.brush); + self.dish.chunk.contents.fill([self.brush; CHUNK_SIZE]); } ui.separator(); @@ -150,9 +147,8 @@ impl eframe::App for UScope { let mut to_remove = None; let mut to_clone = None; - let mut to_update = None; for (i, rule) in self.dish.rules.iter_mut().enumerate() { - let changed = rule_editor( + rule_editor( ui, rule, i, @@ -161,35 +157,25 @@ impl eframe::App for UScope { &mut to_remove, &mut to_clone, ); - if changed { - rule.generate_variants(); - to_update = Some(i); - } - } - if let Some(i) = to_update { - self.dish.update_cache_single_rule(i); } if let Some(i) = to_remove { self.dish.rules.remove(i); - self.dish.rebuild_cache(); } if let Some(i) = to_clone { let mut new_rule = self.dish.rules[i].clone(); new_rule.enabled = false; self.dish.rules.push(new_rule); - self.dish.cache_last_added_rule(); } ui.separator(); if ui.button("add rule").clicked() { self.dish.rules.push(Rule::new()); - self.dish.cache_last_added_rule() } }); }); CentralPanel::default().show(ctx, |ui| { let bounds = ui.available_rect_before_wrap(); let painter = ui.painter_at(bounds); - paint_world(painter, &self.dish, self.show_grid); + paint_chunk(painter, &self.dish.chunk, &self.dish.types, self.show_grid); let rect = ui.allocate_rect(bounds, Sense::click_and_drag()); if let Some(pos) = rect.interact_pointer_pos() { @@ -203,7 +189,6 @@ impl eframe::App for UScope { } } else { self.dish.set_cell(x, y, self.brush); - self.dish.update_cache(x as isize, y as isize, 1, 1); } } }); @@ -211,12 +196,11 @@ impl eframe::App for UScope { } const GRID_SIZE: f32 = 16.; -fn paint_world(painter: Painter, world: &Dish, grid: bool) { - let cells = &world.types; +fn paint_chunk(painter: Painter, chunk: &Chunk, cells: &[CellData], grid: bool) { let bounds = painter.clip_rect(); for x in 0..CHUNK_SIZE { for y in 0..CHUNK_SIZE { - let cell = &world.get_cell(x, y).unwrap(); + let cell = &chunk.get_cell(x, y); let corner = bounds.min + (Vec2::from((x as f32, y as f32)) * GRID_SIZE); let rect = Rect::from_min_size(corner, Vec2::splat(GRID_SIZE)); if cell.id() >= cells.len() { @@ -245,14 +229,11 @@ fn rule_editor( groups: &[CellGroup], to_remove: &mut Option, to_clone: &mut Option, -) -> bool { - let mut changed = false; +) { let id = ui.make_persistent_id(format!("rule {index}")); CollapsingState::load_with_default_open(ui.ctx(), id, true) .show_header(ui, |ui| { - if ui.checkbox(&mut rule.enabled, &rule.name).changed() { - changed = true; - } + ui.checkbox(&mut rule.enabled, &rule.name); if ui.button("delete").clicked() { *to_remove = Some(index); } @@ -264,13 +245,13 @@ fn rule_editor( ui.text_edit_singleline(&mut rule.name); ui.horizontal(|ui| { if ui.checkbox(&mut rule.flip_x, "flip X").changed() { - changed = true; + rule.generate_variants(); } if ui.checkbox(&mut rule.flip_y, "flip Y").changed() { - changed = true; + rule.generate_variants(); } if ui.checkbox(&mut rule.rotate, "rotate").changed() { - changed = true; + rule.generate_variants(); } }); ui.horizontal(|ui| { @@ -316,7 +297,7 @@ fn rule_editor( &mut overlay_lines, ); if changed_left || changed_right { - changed = true; + rule.generate_variants(); } } } @@ -401,7 +382,6 @@ fn rule_editor( ui.painter().line_segment([a, b], stroke); } }); - changed } fn rule_cell_edit_from(