diff --git a/example_rulesets/fire.json b/example_rulesets/fire.json index c0e4f2b..28c2b3e 100644 --- a/example_rulesets/fire.json +++ b/example_rulesets/fire.json @@ -1 +1 @@ -{"rules":[{"name":"new rule","base":{"width":2,"height":1,"contents":[[{"One":1},"None"],[{"One":0},{"One":2}]]},"enabled":true,"flip_x":false,"flip_y":false,"rotate":true,"failrate":0},{"name":"sideways","base":{"width":2,"height":1,"contents":[[{"Group":0},{"One":0}],[{"One":0},{"Copy":[0,0]}]]},"enabled":true,"flip_x":true,"flip_y":false,"rotate":false,"failrate":164},{"name":"up","base":{"width":1,"height":2,"contents":[[{"One":0},{"Copy":[0,1]}],[{"Group":0},"None"]]},"enabled":true,"flip_x":false,"flip_y":false,"rotate":false,"failrate":148},{"name":"new rule","base":{"width":1,"height":1,"contents":[[{"One":2},{"One":3}]]},"enabled":true,"flip_x":false,"flip_y":false,"rotate":false,"failrate":0},{"name":"new rule","base":{"width":1,"height":1,"contents":[[{"One":3},{"One":4}]]},"enabled":true,"flip_x":false,"flip_y":false,"rotate":false,"failrate":0},{"name":"new rule","base":{"width":1,"height":1,"contents":[[{"One":4},{"One":5}]]},"enabled":true,"flip_x":false,"flip_y":false,"rotate":false,"failrate":0},{"name":"new rule","base":{"width":1,"height":1,"contents":[[{"One":5},{"One":0}]]},"enabled":true,"flip_x":false,"flip_y":false,"rotate":false,"failrate":0},{"name":"new rule","base":{"width":1,"height":1,"contents":[[{"One":5},{"One":0}]]},"enabled":true,"flip_x":false,"flip_y":false,"rotate":false,"failrate":0}],"types":[{"name":"air","color":[0,0,0]},{"name":"source","color":[255,255,255]},{"name":"cell #2","color":[255,216,44]},{"name":"cell #3","color":[254,131,0]},{"name":"cell #4","color":[255,73,3]},{"name":"cell #5","color":[255,8,0]}],"groups":[{"name":"fire","void":false,"cells":[5,4,3,2]}]} \ No newline at end of file +{"rules":[{"name":"new rule","base":{"width":2,"height":1,"contents":[[{"One":1},"None"],[{"One":0},{"One":2}]]},"enabled":true,"flip_x":false,"flip_y":false,"rotate":true,"failrate":0},{"name":"sideways","base":{"width":2,"height":1,"contents":[[{"Group":0},{"One":0}],[{"One":0},{"Copy":[0,0]}]]},"enabled":true,"flip_x":true,"flip_y":false,"rotate":false,"failrate":164},{"name":"up","base":{"width":1,"height":2,"contents":[[{"One":0},{"Copy":[0,1]}],[{"Group":0},"None"]]},"enabled":true,"flip_x":false,"flip_y":false,"rotate":false,"failrate":0},{"name":"new rule","base":{"width":1,"height":1,"contents":[[{"One":2},{"One":3}]]},"enabled":true,"flip_x":false,"flip_y":false,"rotate":false,"failrate":0},{"name":"new rule","base":{"width":1,"height":1,"contents":[[{"One":3},{"One":4}]]},"enabled":true,"flip_x":false,"flip_y":false,"rotate":false,"failrate":0},{"name":"new rule","base":{"width":1,"height":1,"contents":[[{"One":4},{"One":5}]]},"enabled":true,"flip_x":false,"flip_y":false,"rotate":false,"failrate":0},{"name":"new rule","base":{"width":1,"height":1,"contents":[[{"One":5},{"One":0}]]},"enabled":true,"flip_x":false,"flip_y":false,"rotate":false,"failrate":0},{"name":"new rule","base":{"width":1,"height":1,"contents":[[{"One":5},{"One":0}]]},"enabled":true,"flip_x":false,"flip_y":false,"rotate":false,"failrate":0}],"types":[{"name":"air","color":[0,0,0]},{"name":"source","color":[255,255,255]},{"name":"cell #2","color":[255,216,44]},{"name":"cell #3","color":[254,131,0]},{"name":"cell #4","color":[255,73,3]},{"name":"cell #5","color":[255,8,0]}],"groups":[{"name":"fire","void":false,"cells":[5,4,3,2]}]} \ No newline at end of file diff --git a/example_rulesets/maze.json b/example_rulesets/maze.json deleted file mode 100644 index 4129c69..0000000 --- a/example_rulesets/maze.json +++ /dev/null @@ -1 +0,0 @@ -{"rules":[{"name":"make maze","base":{"width":1,"height":3,"contents":[[{"One":0},{"One":1}],[{"One":0},{"One":2}],[{"One":1},{"One":1}]]},"enabled":true,"flip_x":false,"flip_y":false,"rotate":true,"failrate":0},{"name":"remove corners","base":{"width":5,"height":5,"contents":[["Any","None"],["Any","None"],[{"Group":0},"None"],["Any","None"],["Any","None"],["Any","None"],["Any","None"],["Any","None"],["Any","None"],["Any","None"],[{"Group":0},"None"],["Any","None"],[{"One":1},{"One":2}],["Any","None"],[{"Group":0},"None"],["Any","None"],["Any","None"],["Any","None"],["Any","None"],["Any","None"],["Any","None"],["Any","None"],[{"Group":0},"None"],["Any","None"],["Any","None"]]},"enabled":true,"flip_x":false,"flip_y":false,"rotate":false,"failrate":0},{"name":"flood fill","base":{"width":1,"height":2,"contents":[[{"One":2},{"Copy":[0,1]}],[{"One":3},"None"]]},"enabled":false,"flip_x":false,"flip_y":false,"rotate":true,"failrate":0},{"name":"snake_move","base":{"width":1,"height":3,"contents":[[{"One":2},{"One":3}],[{"One":2},{"One":4}],[{"One":3},{"One":4}]]},"enabled":true,"flip_x":false,"flip_y":false,"rotate":true,"failrate":0},{"name":"snake_end","base":{"width":3,"height":4,"contents":[["Any","None"],[{"One":4},{"One":5}],["Any","None"],["Any","None"],[{"One":4},{"One":2}],["Any","None"],[{"Group":2},"None"],[{"One":3},{"One":2}],[{"Group":2},"None"],["Any","None"],[{"Group":2},"None"],["Any","None"]]},"enabled":true,"flip_x":false,"flip_y":false,"rotate":true,"failrate":0},{"name":"snake_backtrack","base":{"width":1,"height":3,"contents":[[{"One":4},{"One":5}],[{"One":4},{"One":2}],[{"One":5},{"One":2}]]},"enabled":true,"flip_x":false,"flip_y":false,"rotate":true,"failrate":0},{"name":"snake_reset","base":{"width":3,"height":3,"contents":[["Any","None"],[{"Group":1},"None"],["Any","None"],[{"Group":1},"None"],[{"One":5},{"One":3}],[{"Group":1},"None"],["Any","None"],[{"Group":1},"None"],["Any","None"]]},"enabled":true,"flip_x":false,"flip_y":false,"rotate":false,"failrate":0}],"types":[{"name":"air","color":[0,0,0]},{"name":"maze node","color":[255,147,219]},{"name":"passage","color":[238,202,118]},{"name":"head","color":[0,180,88]},{"name":"tail","color":[183,254,149]},{"name":"rev_head","color":[247,101,240]},{"name":"cell #6","color":[193,0,0]}],"groups":[{"name":"traversed","void":true,"cells":[2,1]},{"name":"not_snake","void":true,"cells":[2,0]},{"name":"air","void":true,"cells":[0]}]} \ No newline at end of file diff --git a/example_rulesets/sparks.json b/example_rulesets/sparks.json deleted file mode 100644 index 6b723ba..0000000 --- a/example_rulesets/sparks.json +++ /dev/null @@ -1 +0,0 @@ -{"rules":[{"name":"move","base":{"width":1,"height":2,"contents":[[{"Group":0},{"One":0}],[{"One":0},{"Copy":[0,0]}]]},"enabled":true,"flip_x":false,"flip_y":false,"rotate":true,"failrate":0},{"name":"spark","base":{"width":1,"height":2,"contents":[[{"One":1},"None"],[{"One":0},{"One":2}]]},"enabled":true,"flip_x":false,"flip_y":false,"rotate":true,"failrate":136},{"name":"fade","base":{"width":1,"height":1,"contents":[[{"One":2},{"One":0}]]},"enabled":true,"flip_x":false,"flip_y":false,"rotate":false,"failrate":174}],"types":[{"name":"air","color":[0,0,0]},{"name":"sparker","color":[255,117,212]},{"name":"spark","color":[244,179,229]}],"groups":[{"name":"empty","void":false,"cells":[2,1]}]} \ No newline at end of file diff --git a/petri/src/lib.rs b/petri/src/lib.rs index bb397c1..a9191a0 100644 --- a/petri/src/lib.rs +++ b/petri/src/lib.rs @@ -74,12 +74,6 @@ pub struct Rule { struct SubRule { width: usize, height: usize, - /// offset from top-left corner used to find and sample matches fairly, normally only used by rotated/mirrored variants - #[serde(default, skip)] - origin_x: usize, - /// offset from top-left corner used to find and sample matches fairly, normally only used by rotated/mirrored variants - #[serde(default, skip)] - origin_y: usize, contents: Vec<(RuleCellFrom, RuleCellTo)>, } @@ -112,8 +106,6 @@ impl SubRule { Self { width: 1, height: 1, - origin_x: 0, - origin_y: 0, contents: vec![Default::default()], } } @@ -244,10 +236,6 @@ impl Rule { self.variants.len() } - pub fn dbg_variants(&self) { - dbg!(&self.variants); - } - pub fn generate_variants(&mut self) { self.variants.clear(); self.variants.push(self.base.clone()); @@ -266,7 +254,6 @@ impl Rule { if self.flip_x { transform_variants(&mut self.variants, |base| { let mut new = base.clone(); - new.origin_x = new.width - new.origin_x - 1; for y in 0..new.height { for x in 0..new.width { let mut cell = base.get(new.width - x - 1, y); @@ -282,7 +269,6 @@ impl Rule { if self.flip_y { transform_variants(&mut self.variants, |base| { let mut new = base.clone(); - new.origin_y = new.height - new.origin_y - 1; for y in 0..new.height { for x in 0..new.width { let mut cell = base.get(x, new.height - y - 1); @@ -299,8 +285,6 @@ impl Rule { // 180° rotations (same as flipping x and y) transform_variants(&mut self.variants, |base| { let mut new = base.clone(); - new.origin_x = new.width - new.origin_x - 1; - new.origin_y = new.height - new.origin_y - 1; for y in 0..new.height { for x in 0..new.width { let mut cell = base.get(new.width - x - 1, new.height - y - 1); @@ -319,8 +303,6 @@ impl Rule { let mut new = base.clone(); new.height = base.width; new.width = base.height; - new.origin_x = base.height - base.origin_y - 1; - new.origin_y = base.origin_x; for y in 0..new.height { for x in 0..new.width { let mut cell = base.get(y, new.width - x - 1); @@ -381,8 +363,6 @@ impl Dish { base: SubRule { width: 1, height: 2, - origin_x: 0, - origin_y: 0, contents: vec![ (RuleCellFrom::One(Cell(1)), RuleCellTo::One(Cell(0))), (RuleCellFrom::One(Cell(0)), RuleCellTo::One(Cell(1))), @@ -396,8 +376,6 @@ impl Dish { base: SubRule { width: 2, height: 2, - origin_x: 0, - origin_y: 0, contents: vec![ (RuleCellFrom::One(Cell(1)), RuleCellTo::One(Cell(0))), (RuleCellFrom::Any, RuleCellTo::None), @@ -437,14 +415,6 @@ impl Dish { new } - pub fn cache_count(&self) -> usize { - self.cache.iter().map(|c| c.matches.len()).sum() - } - - pub fn dbg_cache(&self) { - dbg!(&self.cache); - } - pub fn fill(&mut self, cell: Cell) { self.world.fill(cell); self.rebuild_cache(); @@ -490,14 +460,9 @@ impl Dish { 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 + border_x) { - for py in -border_y..(CHUNK_SIZE as isize + border_y) { - let corner_x = px.wrapping_sub_unsigned(rule.origin_x); - let corner_y = py.wrapping_sub_unsigned(rule.origin_y); - if self - .world - .subrule_matches(corner_x, corner_y, rule, &self.groups) - { + 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)); } } @@ -519,7 +484,7 @@ impl Dish { self.update_match_cache(); } - pub fn update_cache(&mut self, cx: isize, cy: isize, width: usize, height: usize) { + 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), @@ -529,7 +494,7 @@ impl Dish { && y2 < y1.saturating_add_unsigned(h1) && y1 < y2.saturating_add_unsigned(h2) } - let edited_rect = (cx, cy, width, height); + let edited_rect = (x, y, width, height); for cache in &mut self.cache { let rule = &self.rules[cache.rule].variants[cache.variant]; @@ -540,9 +505,7 @@ impl Dish { let mut i = 0; while i < cache.matches.len() { let match_pos = cache.matches[i]; - let m_corner_x = match_pos.0.wrapping_sub_unsigned(rule.origin_x); - let m_corner_y = match_pos.1.wrapping_sub_unsigned(rule.origin_y); - let match_rect = (m_corner_x, m_corner_y, rule_width, rule_height); + 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 { @@ -553,20 +516,9 @@ impl Dish { let border_x = rule_width - 1; let border_y = rule_height - 1; - let x_min = cx.wrapping_sub_unsigned(border_x); - let y_min = cy.wrapping_sub_unsigned(border_y); - let x_max = cx - .wrapping_add_unsigned(width) - .wrapping_add_unsigned(border_x); - let y_max = cy - .wrapping_add_unsigned(height) - .wrapping_add_unsigned(border_y); - - for px in x_min..x_max { - for py in y_min..y_max { - let cx = px.wrapping_sub_unsigned(rule.origin_x); - let cy = py.wrapping_sub_unsigned(rule.origin_y); - if self.world.subrule_matches(cx, cy, rule, &self.groups) { + 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)); } } @@ -598,40 +550,52 @@ impl Dish { let rule = &self.rules[rule_cache.rule].variants[rule_cache.variant]; let width = rule.width; let height = rule.height; - let cx = x.wrapping_sub_unsigned(rule.origin_x); - let cy = y.wrapping_sub_unsigned(rule.origin_y); self.apply_rule(x, y, rule_cache.rule, rule_cache.variant); - self.update_cache(cx, cy, width, height); + 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 origin of a match is outside the bounds. + /// 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 try_one_location(&mut self) { + pub fn fire_blindly_cached(&mut self) { let border_x = self.max_rule_width - 1; let border_y = self.max_rule_height - 1; - let origin_x = ((random::() % (CHUNK_SIZE + border_x * 2)) as isize) + let x = ((random::() % (CHUNK_SIZE + border_x)) as isize) .wrapping_sub_unsigned(border_x); - let origin_y = ((random::() % (CHUNK_SIZE + border_y * 2)) as isize) + let y = ((random::() % (CHUNK_SIZE + border_y)) as isize) .wrapping_sub_unsigned(border_y); - let matches = self.get_matches_at_point(origin_x, origin_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(origin_x, origin_y, rule_index, variant_index); - let variant = &self.rules[rule_index].variants[variant_index]; - let width = variant.width; - let height = variant.height; - self.update_cache( - origin_x.wrapping_sub_unsigned(variant.origin_x), - origin_y.wrapping_sub_unsigned(variant.origin_y), - width, - height, - ); + 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)> { @@ -645,6 +609,62 @@ impl Dish { .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) { + if self.rules.is_empty() { + return; + } + let enabled_rules = self + .rules + .iter() + .enumerate() + .filter_map(|(i, r)| r.enabled.then_some(i)) + .collect::>(); + if enabled_rules.is_empty() { + return; + } + let enabled_rule_index = random::() % enabled_rules.len(); + let rule_index = enabled_rules[enabled_rule_index]; + let rule = &self.rules[rule_index]; + let variant_index = random::() % rule.variants.len(); + let variant = &rule.variants[variant_index].clone(); + let border_x = variant.width - 1; + let border_y = variant.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); + + if self.world.subrule_matches(x, y, variant, &self.groups) { + self.apply_rule(x, y, rule_index, variant_index); + } + } + 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(); @@ -660,25 +680,16 @@ impl Dish { let mut old_state = Vec::new(); for dy in 0..height { for dx in 0..width { - let x = x - .wrapping_add_unsigned(dx) - .wrapping_sub_unsigned(variant.origin_x) as usize; - let y = y - .wrapping_add_unsigned(dy) - .wrapping_sub_unsigned(variant.origin_y) as usize; - old_state.push(self.get_cell(x, y)); + old_state.push( + self.get_cell((x as usize).wrapping_add(dx), (y as usize).wrapping_add(dy)), + ); } } for dx in 0..width { for dy in 0..height { - let px = x - .wrapping_add_unsigned(dx) - .wrapping_sub_unsigned(variant.origin_x) as usize; - let py = y - .wrapping_add_unsigned(dy) - .wrapping_sub_unsigned(variant.origin_y) as usize; - + let px = x.wrapping_add_unsigned(dx) as usize; + let py = y.wrapping_add_unsigned(dy) as usize; match variant.get(dx, dy).1 { RuleCellTo::One(rule_cell) => { self.set_cell(px, py, rule_cell); @@ -738,17 +749,11 @@ impl World { } } - fn subrule_matches( - &self, - corner_x: isize, - corner_y: isize, - subrule: &SubRule, - groups: &[CellGroup], - ) -> bool { + fn subrule_matches(&self, x: isize, y: isize, subrule: &SubRule, groups: &[CellGroup]) -> bool { for dx in 0..subrule.width { for dy in 0..subrule.height { - let x = corner_x.wrapping_add_unsigned(dx) as usize; - let y = corner_y.wrapping_add_unsigned(dy) as usize; + let x = x.wrapping_add_unsigned(dx) as usize; + let y = y.wrapping_add_unsigned(dy) as usize; let cell = self.get_cell(x, y); match subrule.get(dx, dy).0 { RuleCellFrom::One(rule_cell) => { diff --git a/uscope/src/main.rs b/uscope/src/main.rs index aae6a26..dae132c 100644 --- a/uscope/src/main.rs +++ b/uscope/src/main.rs @@ -79,8 +79,12 @@ impl eframe::App for UScope { ctx.request_repaint(); let sim_frame = Instant::now(); for _ in 0..self.speed { - self.dish.try_one_location(); - // self.dish.apply_one_match(); + // benchmarks made with sand_stress_test at 50000 speed in a release build + // ~50ms + self.dish.try_one_position_overlapped(); + // ~35ms + // TODO: has directional bias, figure out why and fix it + // self.dish.fire_blindly_cached(); } let sim_time = sim_frame.elapsed(); // self.sim_times.push(sim_time.as_micros()); @@ -103,9 +107,6 @@ impl eframe::App for UScope { if ui.button("regenerate rules and cache").clicked() { self.dish.update_all_rules(); } - if ui.button("debug cache").clicked() { - self.dish.dbg_cache(); - } ui.horizontal(|ui| { if ui.button("Save").clicked() { self.save_universe(); @@ -222,11 +223,8 @@ impl eframe::App for UScope { self.brush = clicked_cell; } } else { - let old = self.dish.get_cell(x, y); - if Some(self.brush) != old { - self.dish.set_cell(x, y, self.brush); - self.dish.update_cache(x as isize, y as isize, 1, 1); - } + self.dish.set_cell(x, y, self.brush); + self.dish.update_cache(x as isize, y as isize, 1, 1); } } }); @@ -300,9 +298,6 @@ fn rule_editor( ui.label("fail rate:"); ui.add(DragValue::new(&mut rule.failrate)); ui.label(format!("variants: {}", rule.variant_count())); - if ui.button("debug").clicked() { - rule.dbg_variants(); - } }); let cells_y = rule.height(); let cells_x = rule.width();