spread each cycles rule activation attempts over space instead of only considering cached matches

This commit is contained in:
Crispy 2024-05-16 22:55:13 +02:00
parent 3cadbb0984
commit 438efffaf3
2 changed files with 122 additions and 10 deletions

View file

@ -19,6 +19,10 @@ pub struct Dish {
cache: Vec<RuleCache>, cache: Vec<RuleCache>,
#[serde(skip)] #[serde(skip)]
match_cache: Vec<usize>, match_cache: Vec<usize>,
#[serde(skip)]
max_rule_width: usize,
#[serde(skip)]
max_rule_height: usize,
} }
#[derive(Debug)] #[derive(Debug)]
@ -188,6 +192,22 @@ impl Rule {
self.base.width 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) { pub fn resize(&mut self, params: ResizeParam) {
let (dw, dh, dx, dy) = params; let (dw, dh, dx, dy) = params;
@ -369,7 +389,7 @@ impl Dish {
rule.generate_variants() rule.generate_variants()
} }
Self { let mut new = Self {
world: World { world: World {
chunk: Chunk::new().with_random_ones(), chunk: Chunk::new().with_random_ones(),
}, },
@ -385,16 +405,25 @@ impl Dish {
}], }],
cache: Vec::new(), cache: Vec::new(),
match_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) { pub fn fill(&mut self, cell: Cell) {
self.world.fill(cell); self.world.fill(cell);
self.rebuild_cache();
} }
pub fn update_all_rules(&mut self) { pub fn update_all_rules(&mut self) {
self.max_rule_height = 1;
self.max_rule_width = 1;
for rule in &mut self.rules { for rule in &mut self.rules {
rule.generate_variants(); 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(); self.rebuild_cache();
} }
@ -430,7 +459,7 @@ impl Dish {
for px in -border_x..(CHUNK_SIZE as isize) { for px in -border_x..(CHUNK_SIZE as isize) {
for py in -border_y..(CHUNK_SIZE as isize) { for py in -border_y..(CHUNK_SIZE as isize) {
if self.world.subrule_matches(px, py, &rule, &self.groups) { if self.world.subrule_matches(px, py, rule, &self.groups) {
matches.push((px, py)); matches.push((px, py));
} }
} }
@ -444,6 +473,7 @@ impl Dish {
} }
pub fn rebuild_cache(&mut self) { pub fn rebuild_cache(&mut self) {
println!("rebuilding cache");
self.cache.clear(); self.cache.clear();
for rule_index in 0..self.rules.len() { for rule_index in 0..self.rules.len() {
self.add_cache_single_rule(rule_index); self.add_cache_single_rule(rule_index);
@ -485,7 +515,7 @@ impl Dish {
for px in (x.wrapping_sub_unsigned(border_x))..(x.wrapping_add_unsigned(width)) { 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)) { for py in (y.wrapping_sub_unsigned(border_y))..(y.wrapping_add_unsigned(height)) {
if self.world.subrule_matches(px, py, &rule, &self.groups) { if self.world.subrule_matches(px, py, rule, &self.groups) {
cache.matches.push((px, py)); cache.matches.push((px, py));
} }
} }
@ -503,7 +533,8 @@ impl Dish {
.collect(); .collect();
} }
pub fn fire_once(&mut self) { /// 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() { if self.match_cache.is_empty() {
return; return;
} }
@ -521,6 +552,86 @@ impl Dish {
self.update_cache(x, y, 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 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::<usize>() % (CHUNK_SIZE + border_x)) as isize)
.wrapping_sub_unsigned(border_x);
let y = ((random::<usize>() % (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::<usize>() % 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::<usize>() % CHUNK_SIZE) as isize;
let y = (random::<usize>() % CHUNK_SIZE) as isize;
let matches = self.get_matches_containing_point(x, y);
if matches.is_empty() {
return;
}
let i = random::<usize>() % 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) { pub fn fire_blindly(&mut self) {
if self.rules.is_empty() { if self.rules.is_empty() {
return; return;
@ -555,7 +666,8 @@ impl Dish {
let rule = &self.rules[rule_index]; let rule = &self.rules[rule_index];
let variant = &rule.variants[variant_index].clone(); let variant = &rule.variants[variant_index].clone();
if rule.failrate > random() { if rule.failrate != 0 && rule.failrate > random() {
// TODO don't update cache after this
return; return;
} }

View file

@ -29,7 +29,7 @@ fn main() {
struct UScope { struct UScope {
dish: Dish, dish: Dish,
brush: Cell, brush: Cell,
speed: usize, speed: u32,
show_grid: bool, show_grid: bool,
} }
@ -37,7 +37,7 @@ impl UScope {
fn new(_cc: &eframe::CreationContext<'_>) -> Self { fn new(_cc: &eframe::CreationContext<'_>) -> Self {
Self { Self {
dish: Dish::new(), dish: Dish::new(),
speed: 1, speed: 50,
show_grid: false, show_grid: false,
brush: Cell(1), brush: Cell(1),
} }
@ -74,14 +74,14 @@ impl eframe::App for UScope {
fn update(&mut self, ctx: &eframe::egui::Context, _frame: &mut eframe::Frame) { fn update(&mut self, ctx: &eframe::egui::Context, _frame: &mut eframe::Frame) {
ctx.request_repaint(); ctx.request_repaint();
for _ in 0..self.speed { for _ in 0..self.speed {
self.dish.fire_once(); self.dish.try_one_position_overlapped();
} }
SidePanel::left("left_panel") SidePanel::left("left_panel")
.min_width(100.) .min_width(100.)
.show(ctx, |ui| { .show(ctx, |ui| {
ui.heading("Simulation"); ui.heading("Simulation");
ui.label("speed"); ui.label("speed");
ui.add(Slider::new(&mut self.speed, 0..=15).clamp_to_range(false)); ui.add(Slider::new(&mut self.speed, 0..=500).clamp_to_range(false));
ui.checkbox(&mut self.show_grid, "show grid"); ui.checkbox(&mut self.show_grid, "show grid");
if ui.button("regenerate rules and cache").clicked() { if ui.button("regenerate rules and cache").clicked() {
self.dish.update_all_rules(); self.dish.update_all_rules();