Compare commits

...

3 commits

2 changed files with 355 additions and 57 deletions

View file

@ -1,3 +1,5 @@
use std::ops::Not;
use rand::prelude::*;
use serde::{Deserialize, Serialize};
@ -9,10 +11,25 @@ pub struct Cell(pub u16);
#[derive(Debug, Serialize, Deserialize)]
pub struct Dish {
#[serde(skip)]
pub chunk: Chunk,
pub rules: Vec<Rule>,
world: World,
pub rules: Vec<Rule>, // todo make read-only to ensure cache is updated
pub types: Vec<CellData>,
pub groups: Vec<CellGroup>,
pub groups: Vec<CellGroup>, // todo make read-only to ensure cache is updated
#[serde(skip)]
cache: Vec<RuleCache>,
#[serde(skip)]
match_cache: Vec<usize>,
#[serde(skip)]
max_rule_width: usize,
#[serde(skip)]
max_rule_height: usize,
}
#[derive(Debug)]
struct RuleCache {
rule: usize,
variant: usize,
matches: Vec<(isize, isize)>,
}
#[derive(Debug, Default, Serialize, Deserialize)]
@ -29,10 +46,15 @@ pub struct CellData {
}
#[derive(Debug, Default)]
pub struct Chunk {
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)]
@ -170,6 +192,22 @@ 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;
@ -289,7 +327,11 @@ impl Chunk {
}
}
fn fill_random(mut self) -> Self {
fn fill(&mut self, cell: Cell) {
self.contents.fill([cell; CHUNK_SIZE]);
}
fn with_random_ones(mut self) -> Self {
for col in self.contents.iter_mut() {
for cell in col.iter_mut() {
if random::<u8>() % 4 == 0 {
@ -300,7 +342,7 @@ impl Chunk {
self
}
pub fn get_cell(&self, x: usize, y: usize) -> Cell {
fn get_cell(&self, x: usize, y: usize) -> Cell {
self.contents[x][y]
}
@ -347,8 +389,10 @@ impl Dish {
rule.generate_variants()
}
Self {
chunk: Chunk::new().fill_random(),
let mut new = Self {
world: World {
chunk: Chunk::new().with_random_ones(),
},
rules: default_rules,
types: vec![
CellData::new("air", 0, 0, 0),
@ -359,13 +403,233 @@ 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;
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 update_rules(&mut self) {
for rule in &mut self.rules {
rule.generate_variants();
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::<usize>() % self.match_cache.len();
let i = self.match_cache[i];
let rule_cache = &self.cache[i];
let match_pos_index = random::<usize>() % 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::<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) {
@ -381,12 +645,8 @@ impl Dish {
if enabled_rules.is_empty() {
return;
}
let rule = random::<usize>() % enabled_rules.len();
let rule = enabled_rules[rule];
self.fire_rule(rule);
}
fn fire_rule(&mut self, rule_index: usize) {
let enabled_rule_index = random::<usize>() % enabled_rules.len();
let rule_index = enabled_rules[enabled_rule_index];
let rule = &self.rules[rule_index];
let variant_index = random::<usize>() % rule.variants.len();
let variant = &rule.variants[variant_index].clone();
@ -397,17 +657,23 @@ impl Dish {
let y = ((random::<usize>() % (CHUNK_SIZE + border_y)) as isize)
.wrapping_sub_unsigned(border_y);
if !self.subrule_matches(x, y, variant) {
return;
if self.world.subrule_matches(x, y, variant, &self.groups) {
self.apply_rule(x, y, rule_index, variant_index);
}
}
let fail: u8 = random();
if rule.failrate > fail {
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
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 {
@ -436,6 +702,7 @@ 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;
}
@ -451,7 +718,35 @@ impl Dish {
}
}
fn subrule_matches(&self, x: isize, y: isize, subrule: &SubRule) -> bool {
//todo isize
pub fn get_cell(&self, x: usize, y: usize) -> Option<Cell> {
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<Cell> {
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 {
for dx in 0..subrule.width {
for dy in 0..subrule.height {
let x = x.wrapping_add_unsigned(dx) as usize;
@ -464,7 +759,7 @@ impl Dish {
}
}
RuleCellFrom::Group(group_id) => {
let group = &self.groups[group_id];
let group = &groups[group_id];
if let Some(cell) = cell {
if !group.cells.contains(&cell) {
return false;
@ -479,23 +774,6 @@ impl Dish {
}
true
}
//todo isize
pub fn get_cell(&self, x: usize, y: usize) -> Option<Cell> {
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 {

View file

@ -14,7 +14,7 @@ use egui::{collapsing_header::CollapsingState, DragValue, PointerButton};
use native_dialog::FileDialog;
use rand::prelude::*;
use petri::{Cell, CellData, CellGroup, Chunk, Dish, Rule, RuleCellFrom, RuleCellTo, CHUNK_SIZE};
use petri::{Cell, CellData, CellGroup, Dish, Rule, RuleCellFrom, RuleCellTo, CHUNK_SIZE};
fn main() {
eframe::run_native(
@ -29,7 +29,7 @@ fn main() {
struct UScope {
dish: Dish,
brush: Cell,
speed: usize,
speed: u32,
show_grid: bool,
}
@ -37,7 +37,7 @@ impl UScope {
fn new(_cc: &eframe::CreationContext<'_>) -> Self {
Self {
dish: Dish::new(),
speed: 500,
speed: 50,
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_rules();
self.dish.update_all_rules();
}
}
}
@ -74,15 +74,18 @@ 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.fire_blindly();
self.dish.try_one_position_overlapped();
}
SidePanel::left("left_panel")
.min_width(100.)
.show(ctx, |ui| {
ui.heading("Simulation");
ui.label("speed");
ui.add(Slider::new(&mut self.speed, 0..=5000).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");
if ui.button("regenerate rules and cache").clicked() {
self.dish.update_all_rules();
}
ui.horizontal(|ui| {
if ui.button("Save").clicked() {
self.save_universe();
@ -113,7 +116,7 @@ impl eframe::App for UScope {
self.dish.types.push(CellData { name, color })
}
if ui.button("fill").clicked() {
self.dish.chunk.contents.fill([self.brush; CHUNK_SIZE]);
self.dish.fill(self.brush);
}
ui.separator();
@ -147,8 +150,9 @@ 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() {
rule_editor(
let changed = rule_editor(
ui,
rule,
i,
@ -157,25 +161,35 @@ 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_chunk(painter, &self.dish.chunk, &self.dish.types, self.show_grid);
paint_world(painter, &self.dish, self.show_grid);
let rect = ui.allocate_rect(bounds, Sense::click_and_drag());
if let Some(pos) = rect.interact_pointer_pos() {
@ -189,6 +203,7 @@ 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);
}
}
});
@ -196,11 +211,12 @@ impl eframe::App for UScope {
}
const GRID_SIZE: f32 = 16.;
fn paint_chunk(painter: Painter, chunk: &Chunk, cells: &[CellData], grid: bool) {
fn paint_world(painter: Painter, world: &Dish, grid: bool) {
let cells = &world.types;
let bounds = painter.clip_rect();
for x in 0..CHUNK_SIZE {
for y in 0..CHUNK_SIZE {
let cell = &chunk.get_cell(x, y);
let cell = &world.get_cell(x, y).unwrap();
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() {
@ -229,11 +245,14 @@ fn rule_editor(
groups: &[CellGroup],
to_remove: &mut Option<usize>,
to_clone: &mut Option<usize>,
) {
) -> 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| {
ui.checkbox(&mut rule.enabled, &rule.name);
if ui.checkbox(&mut rule.enabled, &rule.name).changed() {
changed = true;
}
if ui.button("delete").clicked() {
*to_remove = Some(index);
}
@ -245,13 +264,13 @@ fn rule_editor(
ui.text_edit_singleline(&mut rule.name);
ui.horizontal(|ui| {
if ui.checkbox(&mut rule.flip_x, "flip X").changed() {
rule.generate_variants();
changed = true;
}
if ui.checkbox(&mut rule.flip_y, "flip Y").changed() {
rule.generate_variants();
changed = true;
}
if ui.checkbox(&mut rule.rotate, "rotate").changed() {
rule.generate_variants();
changed = true;
}
});
ui.horizontal(|ui| {
@ -297,7 +316,7 @@ fn rule_editor(
&mut overlay_lines,
);
if changed_left || changed_right {
rule.generate_variants();
changed = true;
}
}
}
@ -382,6 +401,7 @@ fn rule_editor(
ui.painter().line_segment([a, b], stroke);
}
});
changed
}
fn rule_cell_edit_from(