diff --git a/crate/Cargo.toml b/crate/Cargo.toml index b20d96ae..c278f346 100644 --- a/crate/Cargo.toml +++ b/crate/Cargo.toml @@ -2,6 +2,7 @@ name = "sandtable" version = "0.1.0" authors = ["maxbittker"] +edition = "2021" [lib] crate-type = ["cdylib", "rlib"] @@ -10,9 +11,8 @@ crate-type = ["cdylib", "rlib"] default = ["console_error_panic_hook"] [dependencies] -cfg-if = "0.1.7" +cfg-if = "1.0.0" wasm-bindgen = "0.2.42" -js-sys = "0.3.19" rand = "0.8.3" rand_xoshiro = "0.6.0" @@ -23,7 +23,12 @@ rand_xoshiro = "0.6.0" console_error_panic_hook = { version = "0.1.6", optional = true } [dev-dependencies] -wasm-bindgen-test = "0.2" +wasm-bindgen-test = "0.3.33" +criterion = "0.4.0" + +[[bench]] +name = "benchmarks" +harness = false [profile.release] # Tell `rustc` to optimize for small code size. diff --git a/crate/benches/benchmarks.rs b/crate/benches/benchmarks.rs new file mode 100644 index 00000000..1e97d9fd --- /dev/null +++ b/crate/benches/benchmarks.rs @@ -0,0 +1,34 @@ +use std::time::Duration; + +use criterion::{criterion_group, criterion_main, Criterion}; +use sandtable::{species::Species, universe::Universe}; + +fn setup_universe() -> Universe { + let n = 300; + let h = n / 2; + let d = n / 10 * 9; + + let mut universe = Universe::new(n, n); + + universe.paint(10, 10, 10, Species::Sand); + universe.paint(h, h, d + 2, Species::Plant); + universe.paint(30, n - 10, 15, Species::Fire); + universe.paint(h - 30, n - 10, 15, Species::Fire); + universe.paint(h, h, n / 3, Species::Empty); + universe.paint(h, h, n / 3, Species::Fire); + + universe +} + +fn universe_benchmark(c: &mut Criterion) { + let mut group = c.benchmark_group("Universe"); + group.measurement_time(Duration::from_secs(10)); + + group.bench_function("universe::tick", |b| { + let mut universe = setup_universe(); + b.iter(|| universe.tick()) + }); +} + +criterion_group!(benches, universe_benchmark); +criterion_main!(benches); diff --git a/crate/src/lib.rs b/crate/src/lib.rs index ae090433..3e279e3e 100644 --- a/crate/src/lib.rs +++ b/crate/src/lib.rs @@ -1,20 +1,19 @@ -extern crate cfg_if; -extern crate js_sys; -extern crate rand; -extern crate rand_xoshiro; -extern crate wasm_bindgen; -extern crate web_sys; - -mod species; +pub mod species; +pub mod universe; mod utils; -use rand::{Rng, SeedableRng}; +use rand::Rng; use rand_xoshiro::SplitMix64; use species::Species; -use std::collections::VecDeque; +use universe::UniverseContext; use wasm_bindgen::prelude::*; -// use web_sys::console; +/// An inter-cell force that can ignite cells and move them. +/// +/// - `dx` is the x position of the force. +/// - `dy` is the y position of the force. +/// - `pressure` is the "force" of the force; species have a `pressure` threshold above which they are blown. +/// - `density` is a purely-visual attribute of the force that specifies it's darkness. #[wasm_bindgen] #[repr(C)] #[derive(Clone, Copy, Debug, PartialEq)] @@ -25,6 +24,11 @@ pub struct Wind { density: u8, } +/// A cell that has a species and update logic. +/// +/// - `ra` is a value, defined by the updation logic that stores state and can pass color data to WebGL. +/// - `rb` is a secondary `rb`. +/// - `clock` is a field used to ensure that the cell is not updated twice in one tick. #[wasm_bindgen] #[repr(C)] #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -36,405 +40,32 @@ pub struct Cell { } impl Cell { - pub fn new(species: Species) -> Cell { + /// Create a new `Cell` with some species + /// + /// `ra` is initialized to a random value, for color noise. + /// `rb` is initialized to zero. + /// `clock` is initialized to zero. + #[must_use] + // TODO: `rng` bad, should take reference instead. + pub fn new(species: Species, mut rng: SplitMix64) -> Cell { Cell { - species: species, - ra: 100 + (js_sys::Math::random() * 50.) as u8, + species, + ra: 100 + (rng.gen_range(0..=50)) as u8, rb: 0, clock: 0, } } - pub fn update(&self, api: SandApi) { - self.species.update(*self, api); + + /// Updates the `Cell` using the logic defined in `Species::update` + pub fn update(&self, ctx: &mut UniverseContext) { + self.species.update(*self, ctx); } } -static EMPTY_CELL: Cell = Cell { +/// Defines an empty cell (one with `Species::Empty`), that also doesn't have `ra`/ `rb` set +const EMPTY_CELL: Cell = Cell { species: Species::Empty, ra: 0, rb: 0, clock: 0, }; - -#[wasm_bindgen] -pub struct Universe { - width: i32, - height: i32, - cells: Vec, - undo_stack: VecDeque>, - winds: Vec, - burns: Vec, - generation: u8, - rng: SplitMix64, -} - -pub struct SandApi<'a> { - x: i32, - y: i32, - universe: &'a mut Universe, -} - -impl<'a> SandApi<'a> { - pub fn get(&mut self, dx: i32, dy: i32) -> Cell { - if dx > 2 || dx < -2 || dy > 2 || dy < -2 { - panic!("oob set"); - } - let nx = self.x + dx; - let ny = self.y + dy; - if nx < 0 || nx > self.universe.width - 1 || ny < 0 || ny > self.universe.height - 1 { - return Cell { - species: Species::Wall, - ra: 0, - rb: 0, - clock: self.universe.generation, - }; - } - self.universe.get_cell(nx, ny) - } - pub fn set(&mut self, dx: i32, dy: i32, v: Cell) { - if dx > 2 || dx < -2 || dy > 2 || dy < -2 { - panic!("oob set"); - } - let nx = self.x + dx; - let ny = self.y + dy; - - if nx < 0 || nx > self.universe.width - 1 || ny < 0 || ny > self.universe.height - 1 { - return; - } - let i = self.universe.get_index(nx, ny); - // v.clock += 1; - self.universe.cells[i] = v; - self.universe.cells[i].clock = self.universe.generation.wrapping_add(1); - } - pub fn get_fluid(&mut self) -> Wind { - let idx = self.universe.get_index(self.x, self.y); - - self.universe.winds[idx] - } - pub fn set_fluid(&mut self, v: Wind) { - let idx = self.universe.get_index(self.x, self.y); - - self.universe.burns[idx] = v; - } - - pub fn rand_int(&mut self, n: i32) -> i32 { - self.universe.rng.gen_range(0..n) - } - - pub fn once_in(&mut self, n: i32) -> bool { - self.rand_int(n) == 0 - } - pub fn rand_dir(&mut self) -> i32 { - let i = self.rand_int(1000); - (i % 3) - 1 - } - pub fn rand_dir_2(&mut self) -> i32 { - let i = self.rand_int(1000); - if (i % 2) == 0 { - -1 - } else { - 1 - } - } - - pub fn rand_vec(&mut self) -> (i32, i32) { - let i = self.rand_int(2000); - match i % 9 { - 0 => (1, 1), - 1 => (1, 0), - 2 => (1, -1), - 3 => (0, -1), - 4 => (-1, -1), - 5 => (-1, 0), - 6 => (-1, 1), - 7 => (0, 1), - _ => (0, 0), - } - } - - pub fn rand_vec_8(&mut self) -> (i32, i32) { - let i = self.rand_int(8); - match i { - 0 => (1, 1), - 1 => (1, 0), - 2 => (1, -1), - 3 => (0, -1), - 4 => (-1, -1), - 5 => (-1, 0), - 6 => (-1, 1), - _ => (0, 1), - } - } -} - -#[wasm_bindgen] -impl Universe { - pub fn reset(&mut self) { - for x in 0..self.width { - for y in 0..self.height { - let idx = self.get_index(x, y); - self.cells[idx] = EMPTY_CELL; - } - } - } - pub fn tick(&mut self) { - // let mut next = self.cells.clone(); - // let dx = self.winds[(self.width * self.height / 2) as usize].dx; - // let js: JsValue = (dx).into(); - // console::log_2(&"dx: ".into(), &js); - - for x in 0..self.width { - for y in 0..self.height { - let cell = self.get_cell(x, y); - let wind = self.get_wind(x, y); - Universe::blow_wind( - cell, - wind, - SandApi { - universe: self, - x, - y, - }, - ) - } - } - self.generation = self.generation.wrapping_add(1); - for x in 0..self.width { - let scanx = if self.generation % 2 == 0 { - self.width - (1 + x) - } else { - x - }; - - for y in 0..self.height { - let idx = self.get_index(scanx, y); - let cell = self.get_cell(scanx, y); - - self.burns[idx] = Wind { - dx: 0, - dy: 0, - pressure: 0, - density: 0, - }; - Universe::update_cell( - cell, - SandApi { - universe: self, - x: scanx, - y, - }, - ); - } - } - - self.generation = self.generation.wrapping_add(1); - } - - pub fn width(&self) -> i32 { - self.width - } - - pub fn height(&self) -> i32 { - self.height - } - - pub fn cells(&self) -> *const Cell { - self.cells.as_ptr() - } - - pub fn winds(&self) -> *const Wind { - self.winds.as_ptr() - } - - pub fn burns(&self) -> *const Wind { - self.burns.as_ptr() - } - pub fn paint(&mut self, x: i32, y: i32, size: i32, species: Species) { - let size = size; - let radius: f64 = (size as f64) / 2.0; - - let floor = (radius + 1.0) as i32; - let ciel = (radius + 1.5) as i32; - - for dx in -floor..ciel { - for dy in -floor..ciel { - if (((dx * dx) + (dy * dy)) as f64) > (radius * radius) { - continue; - }; - let px = x + dx; - let py = y + dy; - let i = self.get_index(px, py); - - if px < 0 || px > self.width - 1 || py < 0 || py > self.height - 1 { - continue; - } - if self.get_cell(px, py).species == Species::Empty || species == Species::Empty { - self.cells[i] = Cell { - species: species, - ra: 60 - + (size as u8) - + (self.rng.gen::() * 30.) as u8 - + ((self.generation % 127) as i8 - 60).abs() as u8, - rb: 0, - clock: self.generation, - } - } - } - } - } - - pub fn push_undo(&mut self) { - self.undo_stack.push_front(self.cells.clone()); - self.undo_stack.truncate(50); - } - - pub fn pop_undo(&mut self) { - let old_state = self.undo_stack.pop_front(); - match old_state { - Some(state) => self.cells = state, - None => (), - }; - } - - pub fn flush_undos(&mut self) { - self.undo_stack.clear(); - } - - pub fn new(width: i32, height: i32) -> Universe { - let cells = (0..width * height).map(|_i| EMPTY_CELL).collect(); - let winds: Vec = (0..width * height) - .map(|_i| Wind { - dx: 0, - dy: 0, - pressure: 0, - density: 0, - }) - .collect(); - - let burns: Vec = (0..width * height) - .map(|_i| Wind { - dx: 0, - dy: 0, - pressure: 0, - density: 0, - }) - .collect(); - let rng: SplitMix64 = SeedableRng::seed_from_u64(0x734f6b89de5f83cc); - Universe { - width, - height, - cells, - undo_stack: VecDeque::with_capacity(50), - burns, - winds, - generation: 0, - rng, - } - } -} - -//private methods -impl Universe { - fn get_index(&self, x: i32, y: i32) -> usize { - (x * self.height + y) as usize - } - - fn get_cell(&self, x: i32, y: i32) -> Cell { - let i = self.get_index(x, y); - return self.cells[i]; - } - - fn get_wind(&self, x: i32, y: i32) -> Wind { - let i = self.get_index(x, y); - return self.winds[i]; - } - - fn blow_wind(cell: Cell, wind: Wind, mut api: SandApi) { - if cell.clock - api.universe.generation == 1 { - return; - } - if cell.species == Species::Empty { - return; - } - let mut dx = 0; - let mut dy = 0; - - let threshold = match cell.species { - Species::Empty => 500, - Species::Wall => 500, - Species::Cloner => 500, - - Species::Stone => 70, - Species::Wood => 70, - - Species::Plant => 60, - Species::Lava => 60, - Species::Ice => 60, - - Species::Fungus => 54, - - Species::Oil => 50, - - // Intentionally left out and covered by the default case - // Species::Water => 40, - // Species::Acid => 40, - Species::Seed => 35, - - Species::Sand => 30, - Species::Mite => 30, - Species::Rocket => 30, - - Species::Dust => 10, - Species::Fire => 5, - Species::Gas => 5, - /* - Some hacked species values exist outside of the enum values. - Making sure the default case is emitted allows "BELP" to have a defined wind threshold. - Originally, threshold was a hardcoded value, so this preserves that original glitch behavior. - See: https://sandspiel.club/#eMlYGC52XIto0NM1WjaJ - */ - _ => 40, - }; - - let wx = (wind.dy as i32) - 126; - let wy = (wind.dx as i32) - 126; - - if wx > threshold { - dx = 1; - } - if wy > threshold { - dy = 1; - } - if wx < -threshold { - dx = -1; - } - if wy < -threshold { - dy = -1; - } - if (dx != 0 || dy != 0) && api.get(dx, dy).species == Species::Empty { - api.set(0, 0, EMPTY_CELL); - if dy == -1 - && api.get(dx, -2).species == Species::Empty - && (cell.species == Species::Sand - || cell.species == Species::Water - || cell.species == Species::Lava - || cell.species == Species::Acid - || cell.species == Species::Mite - || cell.species == Species::Dust - || cell.species == Species::Oil - || cell.species == Species::Rocket) - { - dy = -2; - } - api.set(dx, dy, cell); - return; - } - } - fn update_cell(cell: Cell, api: SandApi) { - if cell.clock - api.universe.generation == 1 { - return; - } - - cell.update(api); - } -} diff --git a/crate/src/species.rs b/crate/src/species.rs index 8a44a032..a7ac628e 100644 --- a/crate/src/species.rs +++ b/crate/src/species.rs @@ -1,13 +1,9 @@ -use super::utils::*; -use Cell; -use SandApi; -use Wind; -use EMPTY_CELL; - -// use std::cmp; -use std::mem; +use crate::{universe::UniverseContext, Cell, Wind, EMPTY_CELL}; + +use super::utils::{adjacency_left, adjacency_right, join_dy_dx, split_dy_dx}; + +use std::{cmp::Ordering, mem}; use wasm_bindgen::prelude::*; -// use web_sys::console; #[wasm_bindgen] #[repr(u8)] @@ -17,7 +13,6 @@ pub enum Species { Wall = 1, Sand = 2, Water = 3, - // X = 21, Stone = 13, Ice = 9, Gas = 4, @@ -37,73 +32,73 @@ pub enum Species { } impl Species { - pub fn update(&self, cell: Cell, api: SandApi) { + /// Updates a cell based on it's `Species` + /// + /// Requires a `UniverseContext`, initialized to the `Cell`'s position. + pub fn update(&self, cell: Cell, ctx: &mut UniverseContext) { match self { Species::Empty => {} Species::Wall => {} - Species::Sand => update_sand(cell, api), - Species::Dust => update_dust(cell, api), - Species::Water => update_water(cell, api), - Species::Stone => update_stone(cell, api), - Species::Gas => update_gas(cell, api), - Species::Cloner => update_cloner(cell, api), - Species::Rocket => update_rocket(cell, api), - Species::Fire => update_fire(cell, api), - Species::Wood => update_wood(cell, api), - Species::Lava => update_lava(cell, api), - Species::Ice => update_ice(cell, api), - // Species::Snow => update_ice(cell, api), - //lightning - // Species::Sink => update_sink(cell, api), - Species::Plant => update_plant(cell, api), - Species::Acid => update_acid(cell, api), - Species::Mite => update_mite(cell, api), - Species::Oil => update_oil(cell, api), - Species::Fungus => update_fungus(cell, api), - Species::Seed => update_seed(cell, api), - // Species::X => update_x(cell, api), + Species::Sand => update_sand(cell, ctx), + Species::Water => update_water(cell, ctx), + Species::Gas => update_gas(cell, ctx), + Species::Cloner => update_cloner(cell, ctx), + Species::Fire => update_fire(cell, ctx), + Species::Wood => update_wood(cell, ctx), + Species::Lava => update_lava(cell, ctx), + Species::Ice => update_ice(cell, ctx), + // Species::Sink => update_sink(cell, ctx), + Species::Plant => update_plant(cell, ctx), + Species::Acid => update_acid(cell, ctx), + Species::Stone => update_stone(cell, ctx), + Species::Dust => update_dust(cell, ctx), + Species::Mite => update_mite(cell, ctx), + Species::Oil => update_oil(cell, ctx), + Species::Rocket => update_rocket(cell, ctx), + Species::Fungus => update_fungus(cell, ctx), + Species::Seed => update_seed(cell, ctx), } } } -pub fn update_sand(cell: Cell, mut api: SandApi) { - let dx = api.rand_dir_2(); +pub fn update_sand(cell: Cell, ctx: &mut UniverseContext) { + let dx = ctx.rand_vec1(false); - let nbr = api.get(0, 1); + let nbr = ctx.get(0, 1); if nbr.species == Species::Empty { - api.set(0, 0, EMPTY_CELL); - api.set(0, 1, cell); - } else if api.get(dx, 1).species == Species::Empty { - api.set(0, 0, EMPTY_CELL); - api.set(dx, 1, cell); + ctx.set(0, 0, EMPTY_CELL); + ctx.set(0, 1, cell); + } else if ctx.get(dx, 1).species == Species::Empty { + ctx.set(0, 0, EMPTY_CELL); + ctx.set(dx, 1, cell); } else if nbr.species == Species::Water || nbr.species == Species::Gas || nbr.species == Species::Oil || nbr.species == Species::Acid { - api.set(0, 0, nbr); - api.set(0, 1, cell); + ctx.set(0, 0, nbr); + ctx.set(0, 1, cell); } else { - api.set(0, 0, cell); + ctx.set(0, 0, cell); } } -pub fn update_dust(cell: Cell, mut api: SandApi) { - let dx = api.rand_dir(); - let fluid = api.get_fluid(); +pub fn update_dust(cell: Cell, ctx: &mut UniverseContext) { + let dx = ctx.rand_vec1(true); + let fluid = ctx.get_fluid(); if fluid.pressure > 120 { - api.set( + ctx.set( 0, 0, Cell { species: Species::Fire, - ra: (150 + (cell.ra / 10)) as u8, + ra: (150 + (cell.ra / 10)), rb: 0, clock: 0, }, ); - api.set_fluid(Wind { + ctx.set_fluid(Wind { dx: 0, dy: 0, pressure: 80, @@ -112,29 +107,29 @@ pub fn update_dust(cell: Cell, mut api: SandApi) { return; } - let nbr = api.get(0, 1); + let nbr = ctx.get(0, 1); if nbr.species == Species::Empty { - api.set(0, 0, EMPTY_CELL); - api.set(0, 1, cell); + ctx.set(0, 0, EMPTY_CELL); + ctx.set(0, 1, cell); } else if nbr.species == Species::Water { - api.set(0, 0, nbr); - api.set(0, 1, cell); - } else if api.get(dx, 1).species == Species::Empty { - api.set(0, 0, EMPTY_CELL); - api.set(dx, 1, cell); + ctx.set(0, 0, nbr); + ctx.set(0, 1, cell); + } else if ctx.get(dx, 1).species == Species::Empty { + ctx.set(0, 0, EMPTY_CELL); + ctx.set(dx, 1, cell); } else { - api.set(0, 0, cell); + ctx.set(0, 0, cell); } } -pub fn update_stone(cell: Cell, mut api: SandApi) { - if api.get(-1, -1).species == Species::Stone && api.get(1, -1).species == Species::Stone { +pub fn update_stone(cell: Cell, ctx: &mut UniverseContext) { + if ctx.get(-1, -1).species == Species::Stone && ctx.get(1, -1).species == Species::Stone { return; } - let fluid = api.get_fluid(); + let fluid = ctx.get_fluid(); - if fluid.pressure > 120 && api.rand_int(1) == 0 { - api.set( + if fluid.pressure > 120 && ctx.once_in(2) { + ctx.set( 0, 0, Cell { @@ -147,106 +142,102 @@ pub fn update_stone(cell: Cell, mut api: SandApi) { return; } - let nbr = api.get(0, 1); + let nbr = ctx.get(0, 1); let nbr_species = nbr.species; if nbr_species == Species::Empty { - api.set(0, 0, EMPTY_CELL); - api.set(0, 1, cell); + ctx.set(0, 0, EMPTY_CELL); + ctx.set(0, 1, cell); } else if nbr_species == Species::Water || nbr_species == Species::Gas || nbr_species == Species::Oil || nbr_species == Species::Acid { - api.set(0, 0, nbr); - api.set(0, 1, cell); + ctx.set(0, 0, nbr); + ctx.set(0, 1, cell); } else { - api.set(0, 0, cell); + ctx.set(0, 0, cell); } } -pub fn update_water(cell: Cell, mut api: SandApi) { - let mut dx = api.rand_dir(); - let below = api.get(0, 1); - let dx1 = api.get(dx, 1); - // let mut dx0 = api.get(dx, 0); +pub fn update_water(cell: Cell, ctx: &mut UniverseContext) { + let mut dx = ctx.rand_vec1(true); + let below = ctx.get(0, 1); + let dx1 = ctx.get(dx, 1); + // let mut dx0 = ctx.get(dx, 0); //fall down if below.species == Species::Empty || below.species == Species::Oil { - api.set(0, 0, below); + ctx.set(0, 0, below); let mut ra = cell.ra; - if api.once_in(20) { + if ctx.once_in(20) { //randomize direction when falling sometimes - ra = 100 + api.rand_int(50) as u8; + ra = 100 + ctx.rand_int(0..50) as u8; } - api.set(0, 1, Cell { ra, ..cell }); + ctx.set(0, 1, Cell { ra, ..cell }); return; } else if dx1.species == Species::Empty || dx1.species == Species::Oil { //fall diagonally - api.set(0, 0, dx1); - api.set(dx, 1, cell); + ctx.set(0, 0, dx1); + ctx.set(dx, 1, cell); return; - } else if api.get(-dx, 1).species == Species::Empty { - api.set(0, 0, EMPTY_CELL); - api.set(-dx, 1, cell); + } else if ctx.get(-dx, 1).species == Species::Empty { + ctx.set(0, 0, EMPTY_CELL); + ctx.set(-dx, 1, cell); return; } let left = cell.ra % 2 == 0; dx = if left { 1 } else { -1 }; - let dx0 = api.get(dx, 0); - let dxd = api.get(dx * 2, 0); + let dx0 = ctx.get(dx, 0); + let dxd = ctx.get(dx * 2, 0); if dx0.species == Species::Empty && dxd.species == Species::Empty { // scoot double - api.set(0, 0, dxd); - api.set(2 * dx, 0, Cell { rb: 6, ..cell }); - let (dx, dy) = api.rand_vec_8(); - let nbr = api.get(dx, dy); + ctx.set(0, 0, dxd); + ctx.set(2 * dx, 0, Cell { rb: 6, ..cell }); + let (dx, dy) = ctx.rand_vec2(false); + let nbr = ctx.get(dx, dy); // spread opinion - if nbr.species == Species::Water { - if nbr.ra % 2 != cell.ra % 2 { - api.set( - dx, - dy, - Cell { - ra: cell.ra, - ..cell - }, - ) - } + if nbr.species == Species::Water && nbr.ra % 2 != cell.ra % 2 { + ctx.set( + dx, + dy, + Cell { + ra: cell.ra, + ..cell + }, + ) } } else if dx0.species == Species::Empty || dx0.species == Species::Oil { - api.set(0, 0, dx0); - api.set(dx, 0, Cell { rb: 3, ..cell }); - let (dx, dy) = api.rand_vec_8(); - let nbr = api.get(dx, dy); - if nbr.species == Species::Water { - if nbr.ra % 2 != cell.ra % 2 { - api.set( - dx, - dy, - Cell { - ra: cell.ra, - ..cell - }, - ) - } + ctx.set(0, 0, dx0); + ctx.set(dx, 0, Cell { rb: 3, ..cell }); + let (dx, dy) = ctx.rand_vec2(false); + let nbr = ctx.get(dx, dy); + if nbr.species == Species::Water && nbr.ra % 2 != cell.ra % 2 { + ctx.set( + dx, + dy, + Cell { + ra: cell.ra, + ..cell + }, + ) } } else if cell.rb == 0 { - if api.get(-dx, 0).species == Species::Empty { + if ctx.get(-dx, 0).species == Species::Empty { // bump - api.set( + ctx.set( 0, 0, Cell { - ra: ((cell.ra as i32) + dx) as u8, + ra: (i32::from(cell.ra) + dx) as u8, ..cell }, ); } } else { // become less certain (more bumpable) - api.set( + ctx.set( 0, 0, Cell { @@ -255,38 +246,38 @@ pub fn update_water(cell: Cell, mut api: SandApi) { }, ); } - // if api.once_in(8) { - // let (dx, dy) = api.rand_vec_8(); - // let nbr = api.get(dx, dy); + // if ctx.once_in(8) { + // let (dx, dy) = ctx.rand_vec2(false); + // let nbr = ctx.get(dx, dy); // if nbr.species == Species::Water { // if nbr.ra % 2 != cell.ra % 2 { - // api.set(0, 0, Cell { ra: nbr.ra, ..cell }) + // ctx.set(0, 0, Cell { ra: nbr.ra, ..cell }) // } // } // } - // let (dx, dy) = api.rand_vec_8(); - // let nbr = api.get(dx, dy); + // let (dx, dy) = ctx.rand_vec2(false); + // let nbr = ctx.get(dx, dy); // if nbr.species == Species::Water { - // if nbr.ra % 2 != cell.ra % 2 && api.once_in(2) { - // api.set(0, 0, Cell { ra: nbr.ra, ..cell }) + // if nbr.ra % 2 != cell.ra % 2 && ctx.once_in(2) { + // ctx.set(0, 0, Cell { ra: nbr.ra, ..cell }) // } // } // { - // if api.get(-dx, 0).species == Species::Empty { - // api.set(0, 0, EMPTY_CELL); - // api.set(-dx, 0, cell); + // if ctx.get(-dx, 0).species == Species::Empty { + // ctx.set(0, 0, EMPTY_CELL); + // ctx.set(-dx, 0, cell); // } } -pub fn update_oil(cell: Cell, mut api: SandApi) { +pub fn update_oil(cell: Cell, ctx: &mut UniverseContext) { let rb = cell.rb; - let (dx, dy) = api.rand_vec(); + let (dx, dy) = ctx.rand_vec2(true); let mut new_cell = cell; - let nbr = api.get(dx, dy); + let nbr = ctx.get(dx, dy); if rb == 0 && nbr.species == Species::Fire || nbr.species == Species::Lava || (nbr.species == Species::Oil && nbr.rb > 1 && nbr.rb < 20) @@ -299,96 +290,100 @@ pub fn update_oil(cell: Cell, mut api: SandApi) { }; } - if rb > 1 { - new_cell = Cell { - species: Species::Oil, - ra: cell.ra, - rb: rb - 1, - clock: 0, - }; - api.set_fluid(Wind { - dx: 0, - dy: 10, - pressure: 10, - density: 180, - }); - if rb % 4 != 0 && nbr.species == Species::Empty && nbr.species != Species::Water { - let ra = 20 + api.rand_int(30) as u8; - api.set( - dx, - dy, + match rb.cmp(&1) { + Ordering::Less => (), + Ordering::Equal => { + ctx.set( + 0, + 0, Cell { - species: Species::Fire, - ra, - rb: 0, + species: Species::Empty, + ra: cell.ra, + rb: 90, clock: 0, }, ); + return; } - if nbr.species == Species::Water { + Ordering::Greater => { new_cell = Cell { species: Species::Oil, - ra: 50, - rb: 0, + ra: cell.ra, + rb: rb - 1, clock: 0, }; + ctx.set_fluid(Wind { + dx: 0, + dy: 10, + pressure: 10, + density: 180, + }); + if rb % 4 != 0 && nbr.species == Species::Empty && nbr.species != Species::Water { + let ra = 20 + ctx.rand_int(0..30) as u8; + ctx.set( + dx, + dy, + Cell { + species: Species::Fire, + ra, + rb: 0, + clock: 0, + }, + ); + } + if nbr.species == Species::Water { + new_cell = Cell { + species: Species::Oil, + ra: 50, + rb: 0, + clock: 0, + }; + } } - } else if rb == 1 { - api.set( - 0, - 0, - Cell { - species: Species::Empty, - ra: cell.ra, - rb: 90, - clock: 0, - }, - ); - return; } - if api.get(0, 1).species == Species::Empty { - api.set(0, 0, EMPTY_CELL); - api.set(0, 1, new_cell); - } else if api.get(dx, 1).species == Species::Empty { - api.set(0, 0, EMPTY_CELL); - api.set(dx, 1, new_cell); - } else if api.get(-dx, 1).species == Species::Empty { - api.set(0, 0, EMPTY_CELL); - api.set(-dx, 1, new_cell); - } else if api.get(dx, 0).species == Species::Empty { - api.set(0, 0, EMPTY_CELL); - api.set(dx, 0, new_cell); - } else if api.get(-dx, 0).species == Species::Empty { - api.set(0, 0, EMPTY_CELL); - api.set(-dx, 0, new_cell); + if ctx.get(0, 1).species == Species::Empty { + ctx.set(0, 0, EMPTY_CELL); + ctx.set(0, 1, new_cell); + } else if ctx.get(dx, 1).species == Species::Empty { + ctx.set(0, 0, EMPTY_CELL); + ctx.set(dx, 1, new_cell); + } else if ctx.get(-dx, 1).species == Species::Empty { + ctx.set(0, 0, EMPTY_CELL); + ctx.set(-dx, 1, new_cell); + } else if ctx.get(dx, 0).species == Species::Empty { + ctx.set(0, 0, EMPTY_CELL); + ctx.set(dx, 0, new_cell); + } else if ctx.get(-dx, 0).species == Species::Empty { + ctx.set(0, 0, EMPTY_CELL); + ctx.set(-dx, 0, new_cell); } else { - api.set(0, 0, new_cell); + ctx.set(0, 0, new_cell); } } -pub fn update_gas(cell: Cell, mut api: SandApi) { - let (dx, dy) = api.rand_vec(); +pub fn update_gas(cell: Cell, ctx: &mut UniverseContext) { + let (dx, dy) = ctx.rand_vec2(true); - let nbr = api.get(dx, dy); - // api.set_fluid(Wind { + let nbr = ctx.get(dx, dy); + // ctx.set_fluid(Wind { // dx: 0, // dy: 0, // pressure: 5, // density: 0, // }); if cell.rb == 0 { - api.set(0, 0, Cell { rb: 5, ..cell }); + ctx.set(0, 0, Cell { rb: 5, ..cell }); } if nbr.species == Species::Empty { if cell.rb < 3 { //single molecule - api.set(0, 0, EMPTY_CELL); - api.set(dx, dy, cell); + ctx.set(0, 0, EMPTY_CELL); + ctx.set(dx, dy, cell); } else { - api.set(0, 0, Cell { rb: 1, ..cell }); - api.set( + ctx.set(0, 0, Cell { rb: 1, ..cell }); + ctx.set( dx, dy, Cell { @@ -399,9 +394,9 @@ pub fn update_gas(cell: Cell, mut api: SandApi) { } } else if (dx != 0 || dy != 0) && nbr.species == Species::Gas && nbr.rb < 4 { // if (cell.rb < 2) { - api.set(0, 0, EMPTY_CELL); + ctx.set(0, 0, EMPTY_CELL); // } - api.set( + ctx.set( dx, dy, Cell { @@ -411,33 +406,33 @@ pub fn update_gas(cell: Cell, mut api: SandApi) { ); } } -// pub fn update_x(cell: Cell, mut api: SandApi) { -// let (dx, dy) = api.rand_vec_8(); +// pub fn update_x(cell: Cell, ctx: &mut UniverseContext) { +// let (dx, dy) = ctx.rand_vec2(false); -// let nbr = api.get(dx, dy); +// let nbr = ctx.get(dx, dy); // if nbr.species == Species::X { -// let opposite = api.get(-dx, -dy); +// let opposite = ctx.get(-dx, -dy); // if opposite.species == Species::Empty { -// api.set(0, 0, EMPTY_CELL); -// api.set(-dx, -dy, cell); +// ctx.set(0, 0, EMPTY_CELL); +// ctx.set(-dx, -dy, cell); // } // } // } -pub fn update_cloner(cell: Cell, mut api: SandApi) { - let mut clone_species = unsafe { mem::transmute(cell.rb as u8) }; - let g = api.universe.generation; - for dx in [-1, 0, 1].iter().cloned() { - for dy in [-1, 0, 1].iter().cloned() { +pub fn update_cloner(cell: Cell, ctx: &mut UniverseContext) { + let mut clone_species = unsafe { mem::transmute(cell.rb) }; + let g = ctx.universe_mut().generation(); + for dx in [-1, 0, 1].iter().copied() { + for dy in [-1, 0, 1].iter().copied() { if cell.rb == 0 { - let nbr_species = api.get(dx, dy).species; + let nbr_species = ctx.get(dx, dy).species; if nbr_species != Species::Empty && nbr_species != Species::Cloner && nbr_species != Species::Wall { clone_species = nbr_species; - api.set( + ctx.set( 0, 0, Cell { @@ -450,33 +445,31 @@ pub fn update_cloner(cell: Cell, mut api: SandApi) { break; } - } else { - if api.rand_int(100) > 90 && api.get(dx, dy).species == Species::Empty { - let ra = 80 + api.rand_int(30) as u8 + ((g % 127) as i8 - 60).abs() as u8; - api.set( - dx, - dy, - Cell { - species: clone_species, - ra, - rb: 0, - clock: 0, - }, - ); - break; - } + } else if ctx.rand_int(0..100) > 90 && ctx.get(dx, dy).species == Species::Empty { + let ra = 80 + ctx.rand_int(0..30) as u8 + ((g % 127) as i8 - 60).unsigned_abs(); + ctx.set( + dx, + dy, + Cell { + species: clone_species, + ra, + rb: 0, + clock: 0, + }, + ); + break; } } } } -pub fn update_rocket(cell: Cell, mut api: SandApi) { +pub fn update_rocket(cell: Cell, ctx: &mut UniverseContext) { // rocket has complicated behavior that is staged piecewise in ra. // it would be awesome to diagram the ranges of values and their meaning if cell.rb == 0 { //initialize - api.set( + ctx.set( 0, 0, Cell { @@ -489,13 +482,13 @@ pub fn update_rocket(cell: Cell, mut api: SandApi) { } let clone_species = if cell.rb != 100 { - unsafe { mem::transmute(cell.rb as u8) } + unsafe { mem::transmute(cell.rb) } } else { Species::Sand }; - let (sx, sy) = api.rand_vec(); - let sample = api.get(sx, sy); + let (sx, sy) = ctx.rand_vec2(true); + let sample = ctx.get(sx, sy); if cell.rb == 100 //the type is unset && sample.species != Species::Empty @@ -503,7 +496,7 @@ pub fn update_rocket(cell: Cell, mut api: SandApi) { && sample.species != Species::Wall && sample.species != Species::Cloner { - api.set( + ctx.set( 0, 0, Cell { @@ -519,35 +512,35 @@ pub fn update_rocket(cell: Cell, mut api: SandApi) { if ra == 0 { //falling (dormant) - let dx = api.rand_dir(); - let nbr = api.get(0, 1); + let dx = ctx.rand_vec1(true); + let nbr = ctx.get(0, 1); if nbr.species == Species::Empty { - api.set(0, 0, EMPTY_CELL); - api.set(0, 1, cell); - } else if api.get(dx, 1).species == Species::Empty { - api.set(0, 0, EMPTY_CELL); - api.set(dx, 1, cell); + ctx.set(0, 0, EMPTY_CELL); + ctx.set(0, 1, cell); + } else if ctx.get(dx, 1).species == Species::Empty { + ctx.set(0, 0, EMPTY_CELL); + ctx.set(dx, 1, cell); } else if nbr.species == Species::Water || nbr.species == Species::Gas || nbr.species == Species::Oil || nbr.species == Species::Acid { - api.set(0, 0, nbr); - api.set(0, 1, cell); + ctx.set(0, 0, nbr); + ctx.set(0, 1, cell); } else { - api.set(0, 0, cell); + ctx.set(0, 0, cell); } } else if ra == 1 { //launch - api.set(0, 0, Cell { ra: 2, ..cell }); + ctx.set(0, 0, Cell { ra: 2, ..cell }); } else if ra == 2 { - let (mut dx, mut dy) = api.rand_vec_8(); - let nbr = api.get(dx, dy); + let (mut dx, mut dy) = ctx.rand_vec2(false); + let nbr = ctx.get(dx, dy); if nbr.species != Species::Empty { dx *= -1; dy *= -1; } - api.set( + ctx.set( 0, 0, Cell { @@ -558,22 +551,22 @@ pub fn update_rocket(cell: Cell, mut api: SandApi) { } else if ra > 50 { let (dx, dy) = split_dy_dx(cell.ra - 100); - let nbr = api.get(dx, dy * 2); + let nbr = ctx.get(dx, dy * 2); if nbr.species == Species::Empty || nbr.species == Species::Fire || nbr.species == Species::Rocket { - api.set(0, 0, Cell::new(clone_species)); - api.set(0, dy, Cell::new(clone_species)); + ctx.set(0, 0, Cell::new(clone_species, ctx.universe().rng_cloned())); + ctx.set(0, dy, Cell::new(clone_species, ctx.universe().rng_cloned())); - let (ndx, ndy) = match api.rand_int(100) % 5 { + let (ndx, ndy) = match ctx.rand_int(0..100) % 5 { 0 => adjacency_left((dx, dy)), 1 => adjacency_right((dx, dy)), // 2 => adjacency_right((dx, dy)), _ => (dx, dy), }; - api.set( + ctx.set( dx, dy * 2, Cell { @@ -583,26 +576,26 @@ pub fn update_rocket(cell: Cell, mut api: SandApi) { ); } else { //fizzle - api.set(0, 0, EMPTY_CELL); + ctx.set(0, 0, EMPTY_CELL); } } } -pub fn update_fire(cell: Cell, mut api: SandApi) { +pub fn update_fire(cell: Cell, ctx: &mut UniverseContext) { let ra = cell.ra; - let mut degraded = cell.clone(); - degraded.ra = ra - (2 + api.rand_dir()) as u8; + let mut degraded = cell; + degraded.ra = ra - (2 + ctx.rand_vec1(true)) as u8; - let (dx, dy) = api.rand_vec(); + let (dx, dy) = ctx.rand_vec2(true); - api.set_fluid(Wind { + ctx.set_fluid(Wind { dx: 0, dy: 150, pressure: 1, density: 120, }); - if api.get(dx, dy).species == Species::Gas || api.get(dx, dy).species == Species::Dust { - api.set( + if ctx.get(dx, dy).species == Species::Gas || ctx.get(dx, dy).species == Species::Dust { + ctx.set( dx, dy, Cell { @@ -612,34 +605,34 @@ pub fn update_fire(cell: Cell, mut api: SandApi) { clock: 0, }, ); - api.set_fluid(Wind { + ctx.set_fluid(Wind { dx: 0, dy: 0, pressure: 80, density: 40, }); } - if ra < 5 || api.get(dx, dy).species == Species::Water { - api.set(0, 0, EMPTY_CELL); - } else if api.get(dx, dy).species == Species::Empty { - api.set(0, 0, EMPTY_CELL); - api.set(dx, dy, degraded); + if ra < 5 || ctx.get(dx, dy).species == Species::Water { + ctx.set(0, 0, EMPTY_CELL); + } else if ctx.get(dx, dy).species == Species::Empty { + ctx.set(0, 0, EMPTY_CELL); + ctx.set(dx, dy, degraded); } else { - api.set(0, 0, degraded); + ctx.set(0, 0, degraded); } } -pub fn update_lava(cell: Cell, mut api: SandApi) { - api.set_fluid(Wind { +pub fn update_lava(cell: Cell, ctx: &mut UniverseContext) { + ctx.set_fluid(Wind { dx: 0, dy: 10, pressure: 0, density: 60, }); - let (dx, dy) = api.rand_vec(); + let (dx, dy) = ctx.rand_vec2(true); - if api.get(dx, dy).species == Species::Gas || api.get(dx, dy).species == Species::Dust { - api.set( + if ctx.get(dx, dy).species == Species::Gas || ctx.get(dx, dy).species == Species::Dust { + ctx.set( dx, dy, Cell { @@ -650,9 +643,9 @@ pub fn update_lava(cell: Cell, mut api: SandApi) { }, ); } - let sample = api.get(dx, dy); + let sample = ctx.get(dx, dy); if sample.species == Species::Water { - api.set( + ctx.set( 0, 0, Cell { @@ -662,29 +655,29 @@ pub fn update_lava(cell: Cell, mut api: SandApi) { clock: 0, }, ); - api.set(dx, dy, EMPTY_CELL); - } else if api.get(0, 1).species == Species::Empty { - api.set(0, 0, EMPTY_CELL); - api.set(0, 1, cell); - } else if api.get(dx, 1).species == Species::Empty { - api.set(0, 0, EMPTY_CELL); - api.set(dx, 1, cell); - } else if api.get(dx, 0).species == Species::Empty { - api.set(0, 0, EMPTY_CELL); - api.set(dx, 0, cell); + ctx.set(dx, dy, EMPTY_CELL); + } else if ctx.get(0, 1).species == Species::Empty { + ctx.set(0, 0, EMPTY_CELL); + ctx.set(0, 1, cell); + } else if ctx.get(dx, 1).species == Species::Empty { + ctx.set(0, 0, EMPTY_CELL); + ctx.set(dx, 1, cell); + } else if ctx.get(dx, 0).species == Species::Empty { + ctx.set(0, 0, EMPTY_CELL); + ctx.set(dx, 0, cell); } else { - api.set(0, 0, cell); + ctx.set(0, 0, cell); } } -pub fn update_wood(cell: Cell, mut api: SandApi) { +pub fn update_wood(cell: Cell, ctx: &mut UniverseContext) { let rb = cell.rb; - let (dx, dy) = api.rand_vec(); + let (dx, dy) = ctx.rand_vec2(true); - let nbr_species = api.get(dx, dy).species; + let nbr_species = ctx.get(dx, dy).species; if rb == 0 && nbr_species == Species::Fire || nbr_species == Species::Lava { - api.set( + ctx.set( 0, 0, Cell { @@ -697,7 +690,7 @@ pub fn update_wood(cell: Cell, mut api: SandApi) { } if rb > 1 { - api.set( + ctx.set( 0, 0, Cell { @@ -709,8 +702,8 @@ pub fn update_wood(cell: Cell, mut api: SandApi) { ); if rb % 4 == 0 && nbr_species == Species::Empty { - let ra = 30 + api.rand_int(60) as u8; - api.set( + let ra = 30 + ctx.rand_int(0..60) as u8; + ctx.set( dx, dy, Cell { @@ -722,7 +715,7 @@ pub fn update_wood(cell: Cell, mut api: SandApi) { ) } if nbr_species == Species::Water { - api.set( + ctx.set( 0, 0, Cell { @@ -732,7 +725,7 @@ pub fn update_wood(cell: Cell, mut api: SandApi) { clock: 0, }, ); - api.set_fluid(Wind { + ctx.set_fluid(Wind { dx: 0, dy: 0, pressure: 0, @@ -740,7 +733,7 @@ pub fn update_wood(cell: Cell, mut api: SandApi) { }); } } else if rb == 1 { - api.set( + ctx.set( 0, 0, Cell { @@ -752,15 +745,15 @@ pub fn update_wood(cell: Cell, mut api: SandApi) { ); } } -pub fn update_ice(cell: Cell, mut api: SandApi) { - let (dx, dy) = api.rand_vec(); +pub fn update_ice(cell: Cell, ctx: &mut UniverseContext) { + let (dx, dy) = ctx.rand_vec2(true); - let i = api.rand_int(100); + let i = ctx.rand_int(0..100); - let fluid = api.get_fluid(); + let fluid = ctx.get_fluid(); - if fluid.pressure > 120 && api.rand_int(1) == 0 { - api.set( + if fluid.pressure > 120 && ctx.once_in(2) { + ctx.set( 0, 0, Cell { @@ -773,9 +766,9 @@ pub fn update_ice(cell: Cell, mut api: SandApi) { return; } - let nbr_species = api.get(dx, dy).species; + let nbr_species = ctx.get(dx, dy).species; if nbr_species == Species::Fire || nbr_species == Species::Lava { - api.set( + ctx.set( 0, 0, Cell { @@ -786,7 +779,7 @@ pub fn update_ice(cell: Cell, mut api: SandApi) { }, ); } else if nbr_species == Species::Water && i < 7 { - api.set( + ctx.set( dx, dy, Cell { @@ -799,15 +792,15 @@ pub fn update_ice(cell: Cell, mut api: SandApi) { } } -pub fn update_plant(cell: Cell, mut api: SandApi) { +pub fn update_plant(cell: Cell, ctx: &mut UniverseContext) { let rb = cell.rb; - let mut i = api.rand_int(100); - let (dx, dy) = api.rand_vec(); + let mut i = ctx.rand_int(0..100); + let (dx, dy) = ctx.rand_vec2(true); - let nbr_species = api.get(dx, dy).species; + let nbr_species = ctx.get(dx, dy).species; if rb == 0 && nbr_species == Species::Fire || nbr_species == Species::Lava { - api.set( + ctx.set( 0, 0, Cell { @@ -819,12 +812,12 @@ pub fn update_plant(cell: Cell, mut api: SandApi) { ); } if nbr_species == Species::Wood { - let (dx, dy) = api.rand_vec(); + let (dx, dy) = ctx.rand_vec2(true); let drift = (i % 15) - 7; - let newra = (cell.ra as i32 + drift) as u8; - if api.get(dx, dy).species == Species::Empty { - api.set( + let newra = (i32::from(cell.ra) + drift) as u8; + if ctx.get(dx, dy).species == Species::Empty { + ctx.set( dx, dy, Cell { @@ -836,17 +829,17 @@ pub fn update_plant(cell: Cell, mut api: SandApi) { ); } } - if api.rand_int(100) > 80 + if ctx.once_in(5) && (nbr_species == Species::Water || nbr_species == Species::Fungus - && (api.get(-dx, dy).species == Species::Empty - || api.get(-dx, dy).species == Species::Water - || api.get(-dx, dy).species == Species::Fungus)) + && (ctx.get(-dx, dy).species == Species::Empty + || ctx.get(-dx, dy).species == Species::Water + || ctx.get(-dx, dy).species == Species::Fungus)) { - i = api.rand_int(100); + i = ctx.rand_int(0..100); let drift = (i % 15) - 7; - let newra = (cell.ra as i32 + drift) as u8; - api.set( + let newra = (i32::from(cell.ra) + drift) as u8; + ctx.set( dx, dy, Cell { @@ -855,11 +848,11 @@ pub fn update_plant(cell: Cell, mut api: SandApi) { ..cell }, ); - api.set(-dx, dy, EMPTY_CELL); + ctx.set(-dx, dy, EMPTY_CELL); } if rb > 1 { - api.set( + ctx.set( 0, 0, Cell { @@ -870,8 +863,8 @@ pub fn update_plant(cell: Cell, mut api: SandApi) { ); if nbr_species == Species::Empty { - let ra = 20 + api.rand_int(30) as u8; - api.set( + let ra = 20 + ctx.rand_int(0..30) as u8; + ctx.set( dx, dy, Cell { @@ -883,7 +876,7 @@ pub fn update_plant(cell: Cell, mut api: SandApi) { ); } if nbr_species == Species::Water { - api.set( + ctx.set( 0, 0, Cell { @@ -894,32 +887,32 @@ pub fn update_plant(cell: Cell, mut api: SandApi) { ) } } else if rb == 1 { - api.set(0, 0, EMPTY_CELL); + ctx.set(0, 0, EMPTY_CELL); } let ra = cell.ra; if ra > 50 - && api.get(1, 1).species != Species::Plant - && api.get(-1, 1).species != Species::Plant + && ctx.get(1, 1).species != Species::Plant + && ctx.get(-1, 1).species != Species::Plant { - if api.get(0, 1).species == Species::Empty { - let i = (js_sys::Math::random() * js_sys::Math::random() * 100.) as i32; - let dec = api.rand_int(30) - 20; - if (i + ra as i32) > 165 { - api.set( + if ctx.get(0, 1).species == Species::Empty { + let i = ctx.rand_int(0..=100); + let dec = ctx.rand_int(0..30) - 20; + if (i + i32::from(ra)) > 165 { + ctx.set( 0, 1, Cell { - ra: (ra as i32 + dec) as u8, + ra: (i32::from(ra) + dec) as u8, ..cell }, ); } } else { - api.set( + ctx.set( 0, 0, Cell { - ra: (ra - 1) as u8, + ra: (ra - 1), ..cell }, ); @@ -927,15 +920,15 @@ pub fn update_plant(cell: Cell, mut api: SandApi) { } } -pub fn update_seed(cell: Cell, mut api: SandApi) { +pub fn update_seed(cell: Cell, ctx: &mut UniverseContext) { let rb = cell.rb; let ra = cell.ra; - let (dx, dy) = api.rand_vec(); + let (dx, dy) = ctx.rand_vec2(true); - let nbr_species = api.get(dx, dy).species; + let nbr_species = ctx.get(dx, dy).species; if nbr_species == Species::Fire || nbr_species == Species::Lava { - api.set( + ctx.set( 0, 0, Cell { @@ -951,106 +944,104 @@ pub fn update_seed(cell: Cell, mut api: SandApi) { if rb == 0 { //falling - let dxf = api.rand_dir(); //falling dx - let nbr_species_below = api.get(dxf, 1).species; + let dxf = ctx.rand_vec1(true); //falling dx + let nbr_species_below = ctx.get(dxf, 1).species; if nbr_species_below == Species::Sand || nbr_species_below == Species::Plant || nbr_species_below == Species::Fungus { - let rb = (api.rand_int(253) + 1) as u8; - api.set(0, 0, Cell { rb, ..cell }); + let rb = ctx.rand_int(1..254) as u8; + ctx.set(0, 0, Cell { rb, ..cell }); return; } - let nbr = api.get(0, 1); + let nbr = ctx.get(0, 1); if nbr.species == Species::Empty { - api.set(0, 0, EMPTY_CELL); - api.set(0, 1, cell); - } else if api.get(dxf, 1).species == Species::Empty { - api.set(0, 0, EMPTY_CELL); - api.set(dxf, 1, cell); + ctx.set(0, 0, EMPTY_CELL); + ctx.set(0, 1, cell); + } else if ctx.get(dxf, 1).species == Species::Empty { + ctx.set(0, 0, EMPTY_CELL); + ctx.set(dxf, 1, cell); } else if nbr.species == Species::Water || nbr.species == Species::Gas || nbr.species == Species::Oil || nbr.species == Species::Acid { - api.set(0, 0, nbr); - api.set(0, 1, cell); + ctx.set(0, 0, nbr); + ctx.set(0, 1, cell); } else { - api.set(0, 0, cell); + ctx.set(0, 0, cell); } - } else { - if ra > 60 { - //stem - let dxr = api.rand_dir(); //raising dx - if api.rand_int(100) > 75 { - if (api.get(dxr, -1).species == Species::Empty - || api.get(dxr, -1).species == Species::Sand - || api.get(dxr, -1).species == Species::Seed) - && api.get(1, -1).species != Species::Plant - && api.get(-1, -1).species != Species::Plant - { - let ra = (ra as i32 - api.rand_int(10)) as u8; - api.set(dxr, -1, Cell { ra, ..cell }); - let ra2 = 80 + api.rand_int(30) as u8; - api.set( - 0, - 0, - Cell { - species: Species::Plant, - ra: ra2, - rb: 0, - clock: 0, - }, - ) - } else { - api.set(0, 0, EMPTY_CELL); - } + } else if ra > 60 { + //stem + let dxr = ctx.rand_vec1(true); //raising dx + if ctx.once_in(4) { + if (ctx.get(dxr, -1).species == Species::Empty + || ctx.get(dxr, -1).species == Species::Sand + || ctx.get(dxr, -1).species == Species::Seed) + && ctx.get(1, -1).species != Species::Plant + && ctx.get(-1, -1).species != Species::Plant + { + let ra = (i32::from(ra) - ctx.rand_int(0..10)) as u8; + ctx.set(dxr, -1, Cell { ra, ..cell }); + let ra2 = ctx.rand_int(80..110) as u8; + ctx.set( + 0, + 0, + Cell { + species: Species::Plant, + ra: ra2, + rb: 0, + clock: 0, + }, + ) + } else { + ctx.set(0, 0, EMPTY_CELL); } - } else { - if ra > 40 { - //petals + } + } else if ra > 40 { + //petals - let (mdx, mdy) = api.rand_vec(); + let (mdx, mdy) = ctx.rand_vec2(true); - let (ldx, ldy) = adjacency_left((mdx, mdy)); - let (rdx, rdy) = adjacency_right((mdx, mdy)); + let (ldx, ldy) = adjacency_left((mdx, mdy)); + let (rdx, rdy) = adjacency_right((mdx, mdy)); - if (api.get(mdx, mdy).species == Species::Empty - || api.get(mdx, mdy).species == Species::Plant) - && (api.get(ldx, ldy).species == Species::Empty - || api.get(rdx, rdy).species == Species::Empty) - { - let i = (js_sys::Math::random() * js_sys::Math::random() * 100.) as i32; - let dec = 9 - api.rand_int(3); - if (i + ra as i32) > 100 { - api.set( - mdx, - mdy, - Cell { - ra: (ra as i32 - dec) as u8, - ..cell - }, - ); - } - } - } else { - if nbr_species == Species::Water { - api.set(dx, dy, Cell::new(Species::Seed)) - } + if (ctx.get(mdx, mdy).species == Species::Empty + || ctx.get(mdx, mdy).species == Species::Plant) + && (ctx.get(ldx, ldy).species == Species::Empty + || ctx.get(rdx, rdy).species == Species::Empty) + { + let i = ctx.rand_int(0..=100); + let dec = 9 - ctx.rand_int(0..3); + if (i + i32::from(ra)) > 100 { + ctx.set( + mdx, + mdy, + Cell { + ra: (i32::from(ra) - dec) as u8, + ..cell + }, + ); } } + } else if nbr_species == Species::Water { + ctx.set( + dx, + dy, + Cell::new(Species::Seed, ctx.universe().rng_cloned()), + ) } } -pub fn update_fungus(cell: Cell, mut api: SandApi) { +pub fn update_fungus(cell: Cell, ctx: &mut UniverseContext) { let rb = cell.rb; - let (dx, dy) = api.rand_vec(); + let (dx, dy) = ctx.rand_vec2(true); - let nbr_species = api.get(dx, dy).species; + let nbr_species = ctx.get(dx, dy).species; if rb == 0 && nbr_species == Species::Fire || nbr_species == Species::Lava { - api.set( + ctx.set( 0, 0, Cell { @@ -1061,19 +1052,19 @@ pub fn update_fungus(cell: Cell, mut api: SandApi) { }, ); } - let mut i = api.rand_int(100); + let mut i = ctx.rand_int(0..100); if nbr_species != Species::Empty && nbr_species != Species::Fungus && nbr_species != Species::Fire && nbr_species != Species::Ice { - let (dx, dy) = api.rand_vec(); + let (dx, dy) = ctx.rand_vec2(true); let drift = (i % 15) - 7; - let newra = (cell.ra as i32 + drift) as u8; - if api.get(dx, dy).species == Species::Empty { - api.set( + let newra = (i32::from(cell.ra) + drift) as u8; + if ctx.get(dx, dy).species == Species::Empty { + ctx.set( dx, dy, Cell { @@ -1088,14 +1079,14 @@ pub fn update_fungus(cell: Cell, mut api: SandApi) { if i > 9 && nbr_species == Species::Wood - && api.get(-dx, dy).species == Species::Wood - && api.get(dx, -dy).species == Species::Wood - && api.get(dx, dy).ra % 4 != 0 + && ctx.get(-dx, dy).species == Species::Wood + && ctx.get(dx, -dy).species == Species::Wood + && ctx.get(dx, dy).ra % 4 != 0 { - i = api.rand_int(100); + i = ctx.rand_int(0..100); let drift = (i % 15) - 7; - let newra = (cell.ra as i32 + drift) as u8; - api.set( + let newra = (i32::from(cell.ra) + drift) as u8; + ctx.set( dx, dy, Cell { @@ -1107,7 +1098,7 @@ pub fn update_fungus(cell: Cell, mut api: SandApi) { } if rb > 1 { - api.set( + ctx.set( 0, 0, Cell { @@ -1117,8 +1108,8 @@ pub fn update_fungus(cell: Cell, mut api: SandApi) { }, ); if nbr_species == Species::Empty { - let ra = 10 + api.rand_int(10) as u8; - api.set( + let ra = 10 + ctx.rand_int(0..10) as u8; + ctx.set( dx, dy, Cell { @@ -1130,7 +1121,7 @@ pub fn update_fungus(cell: Cell, mut api: SandApi) { ) } if nbr_species == Species::Water { - api.set( + ctx.set( 0, 0, Cell { @@ -1141,28 +1132,28 @@ pub fn update_fungus(cell: Cell, mut api: SandApi) { ) } } else if rb == 1 { - api.set(0, 0, EMPTY_CELL); + ctx.set(0, 0, EMPTY_CELL); } let ra = cell.ra; if ra > 120 { - let (mdx, mdy) = api.rand_vec(); + let (mdx, mdy) = ctx.rand_vec2(true); let (ldx, ldy) = adjacency_left((mdx, mdy)); let (rdx, rdy) = adjacency_right((mdx, mdy)); - if api.get(mdx, mdy).species == Species::Empty - && api.get(ldx, ldy).species != Species::Fungus - && api.get(rdx, rdy).species != Species::Fungus + if ctx.get(mdx, mdy).species == Species::Empty + && ctx.get(ldx, ldy).species != Species::Fungus + && ctx.get(rdx, rdy).species != Species::Fungus { - let i = (js_sys::Math::random() * js_sys::Math::random() * 100.) as i32; - let dec = 15 - api.rand_int(20); - if (i + ra as i32) > 165 { - api.set( + let i = ctx.rand_int(0..=100); + let dec = 15 - ctx.rand_int(0..20); + if (i + i32::from(ra)) > 165 { + ctx.set( mdx, mdy, Cell { - ra: (ra as i32 - dec) as u8, + ra: (i32::from(ra) - dec) as u8, ..cell }, ); @@ -1171,58 +1162,53 @@ pub fn update_fungus(cell: Cell, mut api: SandApi) { } } -pub fn update_acid(cell: Cell, mut api: SandApi) { - let dx = api.rand_dir(); +pub fn update_acid(cell: Cell, ctx: &mut UniverseContext) { + let dx = ctx.rand_vec1(true); let ra = cell.ra; - let mut degraded = cell.clone(); + let mut degraded = cell; degraded.ra = ra - 60; - // i = api.rand_int(100); + // i = ctx.rand_int(100); if degraded.ra < 80 { degraded = EMPTY_CELL; } - if api.get(0, 1).species == Species::Empty { - api.set(0, 0, EMPTY_CELL); - api.set(0, 1, cell); - } else if api.get(dx, 0).species == Species::Empty { - api.set(0, 0, EMPTY_CELL); - api.set(dx, 0, cell); - } else if api.get(-dx, 0).species == Species::Empty { - api.set(0, 0, EMPTY_CELL); - api.set(-dx, 0, cell); + if ctx.get(0, 1).species == Species::Empty { + ctx.set(0, 0, EMPTY_CELL); + ctx.set(0, 1, cell); + } else if ctx.get(dx, 0).species == Species::Empty { + ctx.set(0, 0, EMPTY_CELL); + ctx.set(dx, 0, cell); + } else if ctx.get(-dx, 0).species == Species::Empty { + ctx.set(0, 0, EMPTY_CELL); + ctx.set(-dx, 0, cell); + } else if ctx.get(0, 1).species != Species::Wall && ctx.get(0, 1).species != Species::Acid { + ctx.set(0, 0, EMPTY_CELL); + ctx.set(0, 1, degraded); + } else if ctx.get(dx, 0).species != Species::Wall && ctx.get(dx, 0).species != Species::Acid { + ctx.set(0, 0, EMPTY_CELL); + ctx.set(dx, 0, degraded); + } else if ctx.get(-dx, 0).species != Species::Wall && ctx.get(-dx, 0).species != Species::Acid { + ctx.set(0, 0, EMPTY_CELL); + ctx.set(-dx, 0, degraded); + } else if ctx.get(0, -1).species != Species::Wall + && ctx.get(0, -1).species != Species::Acid + && ctx.get(0, -1).species != Species::Empty + { + ctx.set(0, 0, EMPTY_CELL); + ctx.set(0, -1, degraded); } else { - if api.get(0, 1).species != Species::Wall && api.get(0, 1).species != Species::Acid { - api.set(0, 0, EMPTY_CELL); - api.set(0, 1, degraded); - } else if api.get(dx, 0).species != Species::Wall && api.get(dx, 0).species != Species::Acid - { - api.set(0, 0, EMPTY_CELL); - api.set(dx, 0, degraded); - } else if api.get(-dx, 0).species != Species::Wall - && api.get(-dx, 0).species != Species::Acid - { - api.set(0, 0, EMPTY_CELL); - api.set(-dx, 0, degraded); - } else if api.get(0, -1).species != Species::Wall - && api.get(0, -1).species != Species::Acid - && api.get(0, -1).species != Species::Empty - { - api.set(0, 0, EMPTY_CELL); - api.set(0, -1, degraded); - } else { - api.set(0, 0, cell); - } + ctx.set(0, 0, cell); } } -pub fn update_mite(cell: Cell, mut api: SandApi) { - let mut i = api.rand_int(100); +pub fn update_mite(cell: Cell, ctx: &mut UniverseContext) { + let mut i = ctx.rand_int(0..100); let mut dx = 0; if cell.ra < 20 { - dx = (cell.ra as i32) - 1; + dx = i32::from(cell.ra) - 1; } let mut dy = 1; - let mut mite = cell.clone(); + let mut mite = cell; if cell.rb > 10 { // / @@ -1235,35 +1221,35 @@ pub fn update_mite(cell: Cell, mut api: SandApi) { // | dx = 0; } - let nbr = api.get(dx, dy); + let nbr = ctx.get(dx, dy); let sx = (i % 3) - 1; - i = api.rand_int(1000); + i = ctx.rand_int(0..1000); let sy = (i % 3) - 1; - let sample = api.get(sx, sy).species; + let sample = ctx.get(sx, sy).species; if sample == Species::Fire || sample == Species::Lava || sample == Species::Water || sample == Species::Oil { - api.set(0, 0, EMPTY_CELL); + ctx.set(0, 0, EMPTY_CELL); return; } if (sample == Species::Plant || sample == Species::Wood || sample == Species::Seed) && i > 800 { - api.set(0, 0, EMPTY_CELL); - api.set(sx, sy, cell); + ctx.set(0, 0, EMPTY_CELL); + ctx.set(sx, sy, cell); return; } if sample == Species::Dust { - api.set(sx, sy, if i > 800 { cell } else { EMPTY_CELL }); + ctx.set(sx, sy, if i > 800 { cell } else { EMPTY_CELL }); } if nbr.species == Species::Empty { - api.set(0, 0, EMPTY_CELL); - api.set(dx, dy, mite); + ctx.set(0, 0, EMPTY_CELL); + ctx.set(dx, dy, mite); } else if dy == 1 && i > 800 { - i = api.rand_int(100); + i = ctx.rand_int(0..100); let mut ndx = (i % 3) - 1; if i < 6 { //switch direction @@ -1273,22 +1259,18 @@ pub fn update_mite(cell: Cell, mut api: SandApi) { mite.ra = (1 + ndx) as u8; mite.rb = 10 + (i % 10) as u8; //hop height - api.set(0, 0, mite); - } else { - if api.get(-1, 0).species == Species::Mite - && api.get(1, 0).species == Species::Mite - && api.get(0, -1).species == Species::Mite - { - api.set(0, 0, EMPTY_CELL); - } else { - if api.get(0, 1).species == Species::Ice { - if api.get(dx, 0).species == Species::Empty { - api.set(0, 0, EMPTY_CELL); - api.set(dx, 0, mite); - } - } else { - api.set(0, 0, mite); - } + ctx.set(0, 0, mite); + } else if ctx.get(-1, 0).species == Species::Mite + && ctx.get(1, 0).species == Species::Mite + && ctx.get(0, -1).species == Species::Mite + { + ctx.set(0, 0, EMPTY_CELL); + } else if ctx.get(0, 1).species == Species::Ice { + if ctx.get(dx, 0).species == Species::Empty { + ctx.set(0, 0, EMPTY_CELL); + ctx.set(dx, 0, mite); } + } else { + ctx.set(0, 0, mite); } } diff --git a/crate/src/universe.rs b/crate/src/universe.rs new file mode 100644 index 00000000..0dd840b7 --- /dev/null +++ b/crate/src/universe.rs @@ -0,0 +1,474 @@ +use std::collections::VecDeque; + +use rand::{distributions::uniform::SampleRange, Rng, SeedableRng}; +use rand_xoshiro::SplitMix64; +use wasm_bindgen::prelude::wasm_bindgen; + +use crate::{species::Species, Cell, Wind, EMPTY_CELL}; + +/// A `Universe`! +/// +/// Contains a 2d rectangle of cells whether cellular automata processes take place; the cornerstone Sandspiel struct. +#[wasm_bindgen] +pub struct Universe { + width: i32, + height: i32, + cells: Vec, + undo_stack: VecDeque>, + winds: Vec, + burns: Vec, + generation: u8, + rng: SplitMix64, +} + +#[cfg_attr(target_arch = "wasm32", wasm_bindgen)] +/// Public methods on `Universe`, many of these are called by JS +impl Universe { + /// Create a new `Universe`. + /// + /// `width` and `height` must both be positive. Note that the larger these get, the more computations are done per frame. + pub fn new(width: i32, height: i32) -> Universe { + let cells = vec![EMPTY_CELL; (width * height) as usize]; + + let empty_wind = Wind { + dx: 0, + dy: 0, + pressure: 0, + density: 0, + }; + + let winds = vec![empty_wind; (width * height) as usize]; + let burns = vec![empty_wind; (width * height) as usize]; + + let rng: SplitMix64 = SeedableRng::seed_from_u64(0x734f_6b89_de5f_83cc); + Universe { + width, + height, + cells, + undo_stack: VecDeque::with_capacity(50), + burns, + winds, + generation: 0, + rng, + } + } + + /// Reset's the `Universe` to a empty state. + /// + /// Note that this does not affect the undo-stack (subject to change). + pub fn reset(&mut self) { + for i in 0..(self.width * self.height) as usize { + self.cells[i] = EMPTY_CELL; + } + } + + /// Updates the `Universe`. + /// + /// Applies two operations to every cell: + /// - Firstly, moves every cell according to the wind under it. + /// - Secondly, calls each cell's update method. ([`Species::update`]) + pub fn tick(&mut self) { + // let mut next = self.cells.clone(); + // let dx = self.winds[(self.width * self.height / 2) as usize].dx; + // let js: JsValue = (dx).into(); + // console::log_2(&"dx: ".into(), &js); + + // blow each cell according to wind + // TODO: we don't need the x/y coords for each cell/ wind. loop over the vec instead. + for x in 0..self.width { + for y in 0..self.height { + let cell = self.get_cell(x, y); + let wind = self.get_wind(x, y); + Universe::blow_wind( + cell, + wind, + &mut UniverseContext { + universe: self, + x, + y, + }, + ) + } + } + self.generation = self.generation.wrapping_add(1); + + // move each cell according to update method + for x in 0..self.width { + // TODO: is this needed? + let scanx = if self.generation % 2 == 0 { + self.width - (1 + x) + } else { + x + }; + + for y in 0..self.height { + let idx = self.get_index(scanx, y); + let cell = self.get_cell(scanx, y); + + self.burns[idx] = Wind { + dx: 0, + dy: 0, + pressure: 0, + density: 0, + }; + Universe::update_cell( + cell, + &mut UniverseContext { + universe: self, + x: scanx, + y, + }, + ); + } + } + + self.generation = self.generation.wrapping_add(1); + } + + /// Returns the width of the `Universe`. + pub fn width(&self) -> i32 { + self.width + } + + /// Returns the height of the `Universe`. + pub fn height(&self) -> i32 { + self.height + } + + /// Returns a raw pointer to the `Universe`'s cells. + // TODO: change this method to `cells_ptr` + pub fn cells(&self) -> *const Cell { + self.cells.as_ptr() + } + + /// Returns a raw pointer to the `Universe`'s winds + // TODO: change this method to `winds_ptr` + pub fn winds(&self) -> *const Wind { + self.winds.as_ptr() + } + + /// Returns a raw pointer to the `Universe`'s burns + // TODO: change this method to `burns_ptr` + pub fn burns(&self) -> *const Wind { + self.burns.as_ptr() + } + + /// Draw a circle of a certain `Species` into the `Universe` + /// + /// - `x` and `y` refer to the center of the circle. + /// - `size` is the diameter of the circle. + /// - `species` is the kind of cell you want to paint. + /// + /// Note that each painted cell has it's own `ra` generated semi-randomly. This causes a noise effect on the resulting circle. + pub fn paint(&mut self, x: i32, y: i32, size: i32, species: Species) { + let size = size; + let radius: f64 = f64::from(size) / 2.0; + + let floor = (radius + 1.0) as i32; + let ciel = (radius + 1.5) as i32; + + for dx in -floor..ciel { + for dy in -floor..ciel { + if f64::from((dx * dx) + (dy * dy)) > (radius * radius) { + continue; + }; + let px = x + dx; + let py = y + dy; + let i = self.get_index(px, py); + + if px < 0 || px > self.width - 1 || py < 0 || py > self.height - 1 { + continue; + } + if self.get_cell(px, py).species == Species::Empty || species == Species::Empty { + self.cells[i] = Cell { + species, + ra: 60 + + (size as u8) + + (self.rng.gen::() * 30.) as u8 + + ((self.generation % 127) as i8 - 60).unsigned_abs(), + rb: 0, + clock: self.generation, + } + } + } + } + } + + /// Save the current `Universe`'s cells to the undo stack + /// + /// Note that the undo stack's size is capped at 50. + pub fn push_undo(&mut self) { + self.undo_stack.push_front(self.cells.clone()); + self.undo_stack.truncate(50); + } + + /// Pop a state from the undo stack and apply it + /// + /// Does nothing is the undo stack is empty. + pub fn pop_undo(&mut self) { + let old_state = self.undo_stack.pop_front(); + if let Some(state) = old_state { + self.cells = state + } + } + + /// Clear the undo stack + pub fn flush_undos(&mut self) { + self.undo_stack.clear(); + } +} + +/// Private helper methods +impl Universe { + /// Converts a xy coord to an index into the `cells` vec + fn get_index(&self, x: i32, y: i32) -> usize { + (x * self.height + y) as usize + } + + /// Returns the [`Copy`]ed cell at the given coords + fn get_cell(&self, x: i32, y: i32) -> Cell { + let i = self.get_index(x, y); + self.cells[i] + } + + /// Returns the [`Copy`]ed wind at the given coords + fn get_wind(&self, x: i32, y: i32) -> Wind { + let i = self.get_index(x, y); + self.winds[i] + } + + /// Moves a cell based on some wind + /// + /// Each cell has a threshold for movement, they are hardcoded here. + /// Also does not update if the cell has already been moved, checked with `cell.clock`. + fn blow_wind(cell: Cell, wind: Wind, ctx: &mut UniverseContext) { + if cell.clock - ctx.universe.generation == 1 { + return; + } + if cell.species == Species::Empty { + return; + } + let mut dx = 0; + let mut dy = 0; + + let threshold = match cell.species { + Species::Empty | Species::Wall | Species::Cloner => i32::MAX, + + Species::Stone | Species::Wood => 70, + + Species::Plant | Species::Lava | Species::Ice => 60, + + Species::Fungus => 54, + + Species::Oil => 50, + + // Intentionally left out and covered by the default case + // Species::Water => 40, + // Species::Acid => 40, + Species::Seed => 35, + + Species::Sand | Species::Mite | Species::Rocket => 30, + + Species::Dust => 10, + Species::Fire | Species::Gas => 5, + /* + Some hacked species values exist outside of the enum values. + Making sure the default case is emitted allows "BELP" to have a defined wind threshold. + Originally, threshold was a hardcoded value, so this preserves that original glitch behavior. + See: https://sandspiel.club/#eMlYGC52XIto0NM1WjaJ + */ + _ => 40, + }; + + let wx = i32::from(wind.dy) - 126; + let wy = i32::from(wind.dx) - 126; + + if wx > threshold { + dx = 1; + } + if wy > threshold { + dy = 1; + } + if wx < -threshold { + dx = -1; + } + if wy < -threshold { + dy = -1; + } + if (dx != 0 || dy != 0) && ctx.get(dx, dy).species == Species::Empty { + ctx.set(0, 0, EMPTY_CELL); + if dy == -1 + && ctx.get(dx, -2).species == Species::Empty + && (cell.species == Species::Sand + || cell.species == Species::Water + || cell.species == Species::Lava + || cell.species == Species::Acid + || cell.species == Species::Mite + || cell.species == Species::Dust + || cell.species == Species::Oil + || cell.species == Species::Rocket) + { + dy = -2; + } + ctx.set(dx, dy, cell); + } + } + + /// Updates a cell with the logic for each species + /// + /// Does not update if the cell has already been moved, checked with `cell.clock`. + fn update_cell(cell: Cell, ctx: &mut UniverseContext) { + if cell.clock - ctx.universe.generation == 1 { + return; + } + + cell.update(ctx); + } + + /// Returns the `Universe`'s internal clock + /// + /// This is used for checking whether a cell has already been updated. + pub fn generation(&self) -> u8 { + self.generation + } + + /// Returns the `Universe`'s P-RNG object + pub fn rng_mut(&mut self) -> &mut SplitMix64 { + &mut self.rng + } + + /// [`Clone`]s and returns the `Universe`'s P-RNG object + pub fn rng_cloned(&self) -> SplitMix64 { + self.rng.clone() + } +} + +/// A context passed to cells when they are updated +/// +/// This provides an API for cells to modify their neighbours and change the `Universe`. +/// The context is also passed the xy coord of the cell being updated, so that relative positions can be used in cell update logic. +pub struct UniverseContext<'a> { + x: i32, + y: i32, + universe: &'a mut Universe, +} + +/// Defines important public function for cell usage. +impl<'a> UniverseContext<'a> { + /// [`Copy`] and return a cell at the *relative* xy coords + /// + /// Currently this implementation is arbitrarily limited to 2 units in each direction; the function panics if further offsets are used. + /// If the resulting position is outside of the `Universe`'s bounds, then a `Species::Wall` is returned. + pub fn get(&mut self, dx: i32, dy: i32) -> Cell { + if !(-2..=2).contains(&dx) || !(-2..=2).contains(&dy) { + panic!("tried to get cell outside of allowed bounds"); + } + let nx = self.x + dx; + let ny = self.y + dy; + if nx < 0 || nx > self.universe.width - 1 || ny < 0 || ny > self.universe.height - 1 { + return Cell { + species: Species::Wall, + ra: 0, + rb: 0, + clock: self.universe.generation, + }; + } + self.universe.get_cell(nx, ny) + } + + /// Sets a cell at the *relative* xy coords + /// + /// Currently this implementation is arbitrarily limited to 2 units in each direction; the function panics if further offsets are used. + /// If the resulting position is outside of the `Universe`'s bounds, then the function silently exits. + /// Also note that cells set with this function will *not* be updated in the same tick again. + pub fn set(&mut self, dx: i32, dy: i32, v: Cell) { + if !(-2..=2).contains(&dx) || !(-2..=2).contains(&dy) { + panic!("tried to set cell outside of allowed bounds"); + } + let nx = self.x + dx; + let ny = self.y + dy; + + if nx < 0 || nx > self.universe.width - 1 || ny < 0 || ny > self.universe.height - 1 { + return; + } + let i = self.universe.get_index(nx, ny); + // v.clock += 1; + self.universe.cells[i] = v; + self.universe.cells[i].clock = self.universe.generation.wrapping_add(1); + } + + /// Gets the fluid ([`Wind`]) at the `UniverseContext`'s position + pub fn get_fluid(&mut self) -> Wind { + let idx = self.universe.get_index(self.x, self.y); + + self.universe.winds[idx] + } + + /// Sets the fluid ([`Wind`]) at the `UniverseContext`'s position + pub fn set_fluid(&mut self, v: Wind) { + let idx = self.universe.get_index(self.x, self.y); + + self.universe.burns[idx] = v; + } + + /// Returns a mutable reference to the `Universe`. + /// + /// This can be used to make further changes, outside of the `UniverseContext` capabilities. + pub fn universe_mut(&mut self) -> &mut Universe { + self.universe + } + + /// Returns a shared (immutable) reference to the `Universe`. + pub fn universe(&self) -> &Universe { + self.universe + } +} + +/// P-RNG helper functions that internally use the `Universe`'s RNG instance +impl UniverseContext<'_> { + /// Generates a random integer in the given range + pub fn rand_int>(&mut self, range: R) -> i32 { + self.universe_mut().rng_mut().gen_range(range) + } + + /// Returns true on average every 1/n times + /// + /// In other words, checks whether a random integer in range `0..n` is zero. + pub fn once_in(&mut self, n: i32) -> bool { + self.rand_int(0..n) == 0 + } + + /// Returns a random 1d unit vector (i.e a single number) + /// + /// The `include_zero` flag expands the possible outputs to include `0`. + pub fn rand_vec1(&mut self, include_zero: bool) -> i32 { + if include_zero { + self.rand_int(-1..=1) + } else { + match self.rand_int(0..=1) { + 0 => -1, + 1 => 1, + _ => unreachable!(), + } + } + } + + /// Returns a random 2d unit vector (i.e a xy direction) + /// + /// The `include_zero` flag expands the possible outputs to include `(0, 0)`. + pub fn rand_vec2(&mut self, include_zero: bool) -> (i32, i32) { + let i = self.rand_int(if include_zero { 0..=8 } else { 0..=7 }); + match i { + 0 => (1, 1), + 1 => (1, 0), + 2 => (1, -1), + 3 => (0, -1), + 4 => (-1, -1), + 5 => (-1, 0), + 6 => (-1, 1), + 7 => (0, 1), + 8 => (0, 0), + _ => unreachable!(), + } + } +} diff --git a/crate/src/utils.rs b/crate/src/utils.rs index 6f82f036..47ec0921 100644 --- a/crate/src/utils.rs +++ b/crate/src/utils.rs @@ -31,10 +31,9 @@ pub fn join_dy_dx(dx: i32, dy: i32) -> u8 { } pub fn split_dy_dx(s: u8) -> (i32, i32) { - let s: i32 = s as i32; + let s: i32 = i32::from(s); let dx: i32 = (s / 3) - 1; - let dy: i32 = (s % 3) - 1; (dx, dy) @@ -48,10 +47,9 @@ cfg_if! { // For more details see // https://github.com/rustwasm/console_error_panic_hook#readme if #[cfg(feature = "console_error_panic_hook")] { - extern crate console_error_panic_hook; - pub use self::console_error_panic_hook::set_once as set_panic_hook; + pub use console_error_panic_hook::set_once as set_panic_hook; } else { #[inline] - pub fn set_panic_hook() {} + pub fn set_panic_hook() {} } }