Skip to content
Snippets Groups Projects
s4_automata.rs 10.9 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 {
        /// Whether we started expanding the grid yet.
        expanding: bool,
        /// 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
                // Seeded such that we get the same pattern every time, which is important since
                // we draw arrows to preset coordinates.
Finn Bear's avatar
Finn Bear committed
                let mut rng = ChaCha8Rng::seed_from_u64(123456789876543216);

                // Randomize grid.
                for y in 0..self.life.size_cells {
                    for x in 0..self.life.size_cells {
                        if rng.gen_bool(0.3) {
                            self.life.set_fill(x, y, Color32::BLACK);
                        }
                    }
                }
            }
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 {
                        expanding: false,
                        iterations: 0,
                    };
                }
            }
            AutomataState::Life { expanding, .. } => {
                if *expanding {
                    // Done with the slide.
                    return true;
                } else {
                    *expanding = 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 {
                expanding,
                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
                    match *iterations {
                        10 => {
                            // Automatically begin expansion.
                            *expanding = true;
Finn Bear's avatar
Finn Bear committed
                        }
Finn Bear's avatar
Finn Bear committed
                        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.
                    let expansion = (*expanding && self.life.size_cells < 64) as usize;
                    self.life = conways_game_of_life(&self.life, expansion);
                    *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.
fn conways_game_of_life(grid: &Grid, expansion: usize) -> 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;
                    }
                    if grid.fill(x, y).a() > 0 {
                        neighbors += 1;
                    }
                }
            }

            let mut alive = grid.fill(cx, cy).a() > 0;

            alive = match (alive, neighbors) {
                (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 {
                    Color32::BLACK
                } else {
                    Color32::TRANSPARENT
                },
            );
        }
    }

    new
}