Skip to content
Snippets Groups Projects
s4_automata.rs 11.6 KiB
Newer Older
Finn Bear's avatar
Finn Bear committed
use crate::color::set_alpha;
use crate::component::arrow::Arrow;
Finn Bear's avatar
Finn Bear committed
use crate::component::grid::Grid;
Finn Bear's avatar
Finn Bear committed
use crate::component::image::Image;
Finn Bear's avatar
Finn Bear committed
use crate::ctx_img;
use crate::egui::{Align2, Context, TextureHandle, Vec2, Window};
use crate::fade_in::{fade_in, fade_in_manual};
Finn Bear's avatar
Finn Bear committed
use crate::governor::Governor;
Finn Bear's avatar
Finn Bear committed
use crate::slide::Slide;
Finn Bear's avatar
Finn Bear committed
use eframe::egui::style::Margin;
use eframe::egui::{Color32, Frame, Ui};
Finn Bear's avatar
Finn Bear committed
use rand::{Rng, SeedableRng};
use rand_chacha::ChaCha8Rng;
Finn Bear's avatar
Finn Bear committed

Finn Bear's avatar
Finn Bear committed
/// Conway's Game of Life, etc.
Finn Bear's avatar
Finn Bear committed
pub struct Automata {
Finn Bear's avatar
Finn Bear committed
    /// Persistent state of the grid.
Finn Bear's avatar
Finn Bear committed
    life: Grid,
Finn Bear's avatar
Finn Bear committed
    /// State of the transitions.
    state: AutomataState,
Finn Bear's avatar
Finn Bear committed
    /// For knowing when to advance the simulation.
Finn Bear's avatar
Finn Bear committed
    governor: Governor,
Finn Bear's avatar
Finn Bear committed
    /// John Conway portrait, loaded lazily.
    conway_portrait: Option<TextureHandle>,
Finn Bear's avatar
Finn Bear committed
}

Finn Bear's avatar
Finn Bear committed
#[derive(Default)]
enum AutomataState {
    /// Nothing on the grid.
    #[default]
    Empty,
    /// Random colors on the grid, but no simulation.
    Static {
        /// When started fading in "alive" arrow.
        alive: Option<f64>,
        /// When started fading in "alive" arrow.
        dead: Option<f64>,
Finn Bear's avatar
Finn Bear committed
        /// When rules started fading in.
        rules: Option<f64>,
Finn Bear's avatar
Finn Bear committed
    },
    /// Simulation.
Finn Bear's avatar
Finn Bear committed
    Life {
Finn Bear's avatar
Finn Bear committed
        /// Whether we inverted the color and turned on fading yet.
        rule_changed: bool,
Finn Bear's avatar
Finn Bear committed
        /// Conway iteration counter.
        iterations: usize,
    },
Finn Bear's avatar
Finn Bear committed
}

Finn Bear's avatar
Finn Bear committed
impl Default for Automata {
Finn Bear's avatar
Finn Bear committed
    fn default() -> Self {
        let mut life = Grid::default();
Finn Bear's avatar
Finn Bear committed
        // Unique identifier for the grid.
Finn Bear's avatar
Finn Bear committed
        life.name = "life";
Finn Bear's avatar
Finn Bear committed

Finn Bear's avatar
Finn Bear committed
        Self {
            life,
Finn Bear's avatar
Finn Bear committed
            state: AutomataState::default(),
Finn Bear's avatar
Finn Bear committed
            governor: Governor::default(),
Finn Bear's avatar
Finn Bear committed
            conway_portrait: None,
Finn Bear's avatar
Finn Bear committed
        }
    }
Finn Bear's avatar
Finn Bear committed
}
Finn Bear's avatar
Finn Bear committed

Finn Bear's avatar
Finn Bear committed
impl Slide for Automata {
Finn Bear's avatar
Finn Bear committed
    fn transition(&mut self, ctx: &Context) -> bool {
        match &mut self.state {
            AutomataState::Empty => {
                self.state = AutomataState::Static {
                    alive: None,
                    dead: None,
Finn Bear's avatar
Finn Bear committed
                    rules: None,
Finn Bear's avatar
Finn Bear committed
                };

Finn Bear's avatar
Finn Bear committed
                randomize_grid(&mut self.life);
Finn Bear's avatar
Finn Bear committed
            }
Finn Bear's avatar
Finn Bear committed
            AutomataState::Static { alive, dead, rules } => {
                // Enable arrows/text one by one.
Finn Bear's avatar
Finn Bear committed
                if alive.is_none() {
                    *alive = Some(ctx.input().time);
                } else if dead.is_none() {
                    *dead = Some(ctx.input().time);
Finn Bear's avatar
Finn Bear committed
                } else if rules.is_none() {
                    *rules = Some(ctx.input().time)
Finn Bear's avatar
Finn Bear committed
                } else {
Finn Bear's avatar
Finn Bear committed
                    self.state = AutomataState::Life {
Finn Bear's avatar
Finn Bear committed
                        rule_changed: false,
Finn Bear's avatar
Finn Bear committed
                        iterations: 0,
                    };
                }
            }
Finn Bear's avatar
Finn Bear committed
            AutomataState::Life { rule_changed, .. } => {
                if !*rule_changed {
                    *rule_changed = true;
                } else {
Finn Bear's avatar
Finn Bear committed
                    // Done with the slide.
                    return true;
Finn Bear's avatar
Finn Bear committed
                }
            }
        }
        false
    }

    fn show(&mut self, ui: &mut Ui) {
Finn Bear's avatar
Finn Bear committed
        Frame::none().margin(Margin::same(20.0)).show(ui, |ui| {
            ui.vertical_centered(|ui| {
                ui.heading("Cellular Automata");
            });
        });

Finn Bear's avatar
Finn Bear committed
        // TODO: Fade/move/position as appropriate.
Finn Bear's avatar
Finn Bear committed
        Image::default().show(
            ui,
            self.conway_portrait
                .get_or_insert_with(|| ctx_img!(ui.ctx(), "conway_portrait.png")),
        );
Finn Bear's avatar
Finn Bear committed

Finn Bear's avatar
Finn Bear committed
        // Need to continuously animate the grid, or at least poll the governor.
        ui.ctx().request_repaint();
Finn Bear's avatar
Finn Bear committed

Finn Bear's avatar
Finn Bear committed
        // If [`None`], don't show rules. If [`Some`], fade in rules as if they started fading in
        // at this time.
        let mut rules_fade: Option<f64> = None;

        // How much grid goes left and rules go right.
        const HORIZONTAL_OFFSET: f32 = 250.0;

Finn Bear's avatar
Finn Bear committed
        match &mut self.state {
Finn Bear's avatar
Finn Bear committed
            AutomataState::Empty => {}
Finn Bear's avatar
Finn Bear committed
            AutomataState::Static { rules, .. } => {
Finn Bear's avatar
Finn Bear committed
                if let Some(rules) = *rules {
Finn Bear's avatar
Finn Bear committed
                    let elapsed = ui.ctx().input().time - rules;
                    self.life.offset_x = (elapsed as f32 * -400.0).max(-HORIZONTAL_OFFSET);
                    rules_fade = Some(rules);
                }
            }
Finn Bear's avatar
Finn Bear committed
            AutomataState::Life {
Finn Bear's avatar
Finn Bear committed
                rule_changed,
Finn Bear's avatar
Finn Bear committed
                iterations,
            } => {
Finn Bear's avatar
Finn Bear committed
                // Iterate grid ~5 times a second.
                if self.governor.ready(ui.ctx().input().time, 0.2) {
Finn Bear's avatar
Finn Bear committed
                    // Schedule some special events at some iteration counts.
Finn Bear's avatar
Finn Bear committed
                    let expanding = *iterations >= 10;
Finn Bear's avatar
Finn Bear committed
                    match *iterations {
                        20 => {
                            for y in 4..self.life.size_cells - 4 {
                                // Line on the left.
                                self.life.set_fill(2, y, Color32::BLACK);

                                // Line with gaps on the right.
                                if y < self.life.size_cells - 11 && y % 8 != 0 {
                                    self.life
                                        .set_fill(self.life.size_cells - 2, y, Color32::BLACK);
                                }
                            }
                        }
                        30 => {
                            // Draw another set of lines to create a cool pattern.
                            for x in 4..self.life.size_cells - 4 {
                                // Solid line at the bottom.
                                self.life
                                    .set_fill(x, self.life.size_cells - 2, Color32::BLACK);
                                // Line with gaps at the top.
                                if x % 6 != 0 {
                                    self.life.set_fill(x, 2, Color32::BLACK);
                                }
Finn Bear's avatar
Finn Bear committed
                        }
Finn Bear's avatar
Finn Bear committed
                        _ => {}
Finn Bear's avatar
Finn Bear committed
                    }

                    // Boolean to int cast yields 1 if true, 0 otherwise.
Finn Bear's avatar
Finn Bear committed
                    let expansion = (expanding && self.life.size_cells < 64) as usize;

                    // Implement rule changes.
                    let (background_color, alive_color, stroke_color, fade) = if *rule_changed {
                        (Color32::BLACK, Color32::WHITE, Color32::TRANSPARENT, true)
                    } else {
                        (Color32::TRANSPARENT, Color32::BLACK, Color32::GRAY, false)
                    };
                    self.life.background = background_color;
                    self.life.stroke = stroke_color;
                    self.life = conways_game_of_life(&self.life, expansion, alive_color, fade);
Finn Bear's avatar
Finn Bear committed
                    *iterations += 1;
Finn Bear's avatar
Finn Bear committed
                }
Finn Bear's avatar
Finn Bear committed

                // Kludge to render rules with full opacity.
                rules_fade = Some(0.0);
Finn Bear's avatar
Finn Bear committed
            }
Finn Bear's avatar
Finn Bear committed
        }
Finn Bear's avatar
Finn Bear committed

Finn Bear's avatar
Finn Bear committed
        if let Some(rules_fade) = rules_fade {
Finn Bear's avatar
Finn Bear committed
            // Give some time for the grid to slide left.
            const RULES_DELAY: f64 = 0.3;
            fade_in(ui, rules_fade + RULES_DELAY, |ui| {
Finn Bear's avatar
Finn Bear committed
                Window::new("conway_rules")
Finn Bear's avatar
Finn Bear committed
                    .frame(Frame::window(&ui.style()))
Finn Bear's avatar
Finn Bear committed
                    .anchor(Align2::CENTER_CENTER, Vec2::new(HORIZONTAL_OFFSET, 0.0))
                    .title_bar(false)
                    .resizable(false)
                    .show(ui.ctx(), |ui| {
Finn Bear's avatar
Finn Bear committed
                        // Nested fade in since fade doesn't propagate from [`Window`] to children.
Finn Bear's avatar
Finn Bear committed
                        fade_in(ui, rules_fade + RULES_DELAY, |ui| {
Finn Bear's avatar
Finn Bear committed
                            ui.label(" ⏵ Cells with fewer than two neighbors die");
                            ui.label(" ⏵ Cells with more than three neighbors die");
                            ui.label(" ⏵ Cells with three neighbors become alive");
                        })
Finn Bear's avatar
Finn Bear committed
                    });
            });
        }

Finn Bear's avatar
Finn Bear committed
        self.life.show_with_overlays(ui.ctx(), |ui, to_screen| {
Finn Bear's avatar
Finn Bear committed
            if let &AutomataState::Static { alive, dead, .. } = &self.state {
Finn Bear's avatar
Finn Bear committed
                if let Some(alive) = alive {
                    fade_in_manual(ui, alive, |ui, alpha| {
                        let color = set_alpha(Color32::GREEN, alpha);
                        Arrow {
                            origin: to_screen * self.life.center(4, 2),
                            tip: to_screen * self.life.center(7, 7),
Finn Bear's avatar
Finn Bear committed
                            stroke: color,
                            label: "Alive".into(),
                            ..Arrow::default()
Finn Bear's avatar
Finn Bear committed
                        }
                        .show(ui);
                    });
                }
                if let Some(dead) = dead {
                    fade_in_manual(ui, dead, |ui, alpha| {
                        let color = set_alpha(Color32::DARK_GREEN, alpha);
                        Arrow {
                            origin: to_screen * self.life.center(11, 13),
                            tip: to_screen * self.life.center(10, 10),
Finn Bear's avatar
Finn Bear committed
                            stroke: color,
                            label: "Dead".into(),
                            ..Arrow::default()
Finn Bear's avatar
Finn Bear committed
                        }
                        .show(ui);
                    });
                }
            }
        });
Finn Bear's avatar
Finn Bear committed
    }
}
Finn Bear's avatar
Finn Bear committed

Finn Bear's avatar
Finn Bear committed
/// Run one iteration of Conway's Game of Life on the grid, producing a new grid.
Finn Bear's avatar
Finn Bear committed
///
/// A non-zero expansion parameter expands the grid on each side by that many squares.
Finn Bear's avatar
Finn Bear committed
fn conways_game_of_life(grid: &Grid, expansion: usize, alive_color: Color32, fade: bool) -> Grid {
Finn Bear's avatar
Finn Bear committed
    let mut new = grid.clone();
    new.clear_fill();

Finn Bear's avatar
Finn Bear committed
    // Allocate more room on left, right, top and bottom.
    new.size_cells += expansion * 2;

Finn Bear's avatar
Finn Bear committed
    for cy in 0..grid.size_cells {
        for cx in 0..grid.size_cells {
            let mut neighbors = 0;
            for dy in -1..=1 {
                for dx in -1..=1 {
                    let (x, y) = match (cx.checked_add_signed(dx), cy.checked_add_signed(dy)) {
                        (Some(x), Some(y)) if x < grid.size_cells && y < grid.size_cells => (x, y),
                        _ => continue,
                    };
                    if x == cx && y == cy {
                        // Don't consider self.
                        continue;
                    }
Finn Bear's avatar
Finn Bear committed
                    if grid.fill(x, y).is_opaque() {
Finn Bear's avatar
Finn Bear committed
                        neighbors += 1;
                    }
                }
            }

Finn Bear's avatar
Finn Bear committed
            let was_alive = grid.fill(cx, cy).is_opaque();
Finn Bear's avatar
Finn Bear committed

Finn Bear's avatar
Finn Bear committed
            let alive = match (was_alive, neighbors) {
Finn Bear's avatar
Finn Bear committed
                (false, 3) => true,
                (false, _) => false,
                (true, 2) | (true, 3) => true,
                (true, _) => false,
            };

Finn Bear's avatar
Finn Bear committed
            // Write back to the middle of the expanded area to avoid shift.
Finn Bear's avatar
Finn Bear committed
            new.set_fill(
Finn Bear's avatar
Finn Bear committed
                cx + expansion,
                cy + expansion,
Finn Bear's avatar
Finn Bear committed
                if alive {
Finn Bear's avatar
Finn Bear committed
                    alive_color
                } else if !fade {
Finn Bear's avatar
Finn Bear committed
                    Color32::TRANSPARENT
Finn Bear's avatar
Finn Bear committed
                } else {
                    // Fade out gradually.
                    let previous_alpha = grid.fill(cx, cy).a();
                    set_alpha(alive_color, previous_alpha as f32 / (255.0 * 2.0))
Finn Bear's avatar
Finn Bear committed

// Sets a deterministic subset of the grid (approximately 30% of it) to black.
pub fn randomize_grid(grid: &mut Grid) {
    // Seeded such that we get the same pattern every time, which is important since
    // we draw arrows to preset coordinates.
    let mut rng = ChaCha8Rng::seed_from_u64(123456789876543216);

    // Randomize grid.
    for y in 0..grid.size_cells {
        for x in 0..grid.size_cells {
            if rng.gen_bool(0.3) {
                grid.set_fill(x, y, Color32::BLACK);
            }
        }
    }
}