summaryrefslogtreecommitdiff
path: root/src/sim/cell
diff options
context:
space:
mode:
authorKai Stevenson <kai@kaistevenson.com>2026-08-16 17:29:42 -0700
committerKai Stevenson <kai@kaistevenson.com>2026-08-16 17:29:42 -0700
commitf177cc716c5f2aa5b50b14ccbb421de89e3a7854 (patch)
treedb06f9f44251d6d90e5e9709d47e6cc4397b9443 /src/sim/cell
parent40e9d818195824749293dff3afcfdd5c1432adbe (diff)
wip
Diffstat (limited to 'src/sim/cell')
-rw-r--r--src/sim/cell/cell.rs38
-rw-r--r--src/sim/cell/materials/fire.rs79
-rw-r--r--src/sim/cell/materials/gas.rs76
-rw-r--r--src/sim/cell/materials/mod.rs88
-rw-r--r--src/sim/cell/materials/sand.rs10
-rw-r--r--src/sim/cell/materials/smoke.rs20
-rw-r--r--src/sim/cell/materials/water.rs76
-rw-r--r--src/sim/cell/mod.rs2
8 files changed, 389 insertions, 0 deletions
diff --git a/src/sim/cell/cell.rs b/src/sim/cell/cell.rs
new file mode 100644
index 0000000..d35fa76
--- /dev/null
+++ b/src/sim/cell/cell.rs
@@ -0,0 +1,38 @@
+use crate::sim::cell::materials::MaterialId;
+
+#[derive(Clone, Copy)]
+pub struct Cell {
+ pub material: MaterialId,
+ pub flags: u8,
+ pub data: u16,
+}
+
+impl Cell {
+ const FLAG_PARITY: u8 = 0b0000_0001;
+
+ #[inline]
+ pub fn parity(self) -> u8 {
+ self.flags & Self::FLAG_PARITY
+ }
+ #[inline]
+ pub fn flip_parity(&mut self) {
+ self.flags ^= Self::FLAG_PARITY;
+ }
+}
+
+impl Cell {
+ pub fn void() -> Cell {
+ Cell {
+ material: MaterialId::Void,
+ flags: 0,
+ data: 0,
+ }
+ }
+ pub fn from_material(material: MaterialId) -> Cell {
+ Cell {
+ material,
+ flags: 0,
+ data: 0,
+ }
+ }
+}
diff --git a/src/sim/cell/materials/fire.rs b/src/sim/cell/materials/fire.rs
new file mode 100644
index 0000000..e0416b0
--- /dev/null
+++ b/src/sim/cell/materials/fire.rs
@@ -0,0 +1,79 @@
+use rand::RngExt;
+
+use crate::sim::{
+ cell::{cell::Cell, materials::MaterialId},
+ cell_sim::sim::UpdateCtx,
+};
+
+trait FireCellView {
+ fn get_ticks_lived(self) -> u16;
+ fn set_ticks_lived(&mut self, ticks: u16) -> ();
+ fn is_flammable(self) -> bool;
+}
+
+impl FireCellView for Cell {
+ fn get_ticks_lived(self) -> u16 {
+ self.data
+ }
+ fn set_ticks_lived(&mut self, ticks: u16) {
+ self.data = ticks;
+ }
+ // this could be a property of the material def, I think it's better here for now
+ fn is_flammable(self) -> bool {
+ [MaterialId::Wood].contains(&self.material)
+ }
+}
+
+// TODO optimize number of rng calls?
+#[inline]
+pub fn sim_update(ctx: &mut UpdateCtx) {
+ let ticks_lived = ctx.cell.get_ticks_lived();
+ // 2 seconds
+ if ticks_lived > 240 {
+ // we have a chance to live longer--roughly 50% chance of living an extra second
+ if ctx.rng.random_range(0.0..1.0) > 0.995 {
+ // kill ourselves, with a chance to turn into ash
+ if ctx.rng.random_range(0.0..1.0) > 0.8 {
+ // TODO ash material
+ ctx.set_cell(0, 0, Cell::from_material(MaterialId::Sand));
+ } else {
+ ctx.set_cell(0, 0, Cell::void());
+ }
+ return;
+ }
+ }
+
+ // at an average of 4 times per lifespan, try to spread
+ // 1/60 * 240 = 4
+ if ctx.rng.random_range(0.0..1.0) > (59.0 / 60.0) {
+ let (dx, dy) = (ctx.rng.random_range(-1..=1), ctx.rng.random_range(-1..=1));
+ if let Some(target) = ctx.get_cell(dx, dy)
+ && target.is_flammable()
+ {
+ ctx.set_cell(dx, dy, Cell::from_material(MaterialId::Fire));
+ }
+ }
+
+ // 8 times in our lifespan, emit smoke
+ if ticks_lived.is_multiple_of(30)
+ && let Some(target) = ctx.get_cell(0, -1)
+ && target.material == MaterialId::Void
+ {
+ for (dx, dy) in [
+ (0, -1),
+ (1 - ctx.seqno_parity as i32 * 2, 0),
+ (-1 + ctx.seqno_parity as i32 * 2, 0),
+ (0, 1),
+ ] {
+ if let Some(target) = ctx.get_cell(dx, dy)
+ && target.material == MaterialId::Void
+ {
+ ctx.set_cell(dx, dy, Cell::from_material(MaterialId::Smoke));
+ break;
+ }
+ }
+ }
+
+ ctx.cell.set_ticks_lived(ticks_lived + 1);
+ ctx.set_cell(0, 0, *ctx.cell);
+}
diff --git a/src/sim/cell/materials/gas.rs b/src/sim/cell/materials/gas.rs
new file mode 100644
index 0000000..c0cd9f9
--- /dev/null
+++ b/src/sim/cell/materials/gas.rs
@@ -0,0 +1,76 @@
+use crate::sim::cell_sim::sim::UpdateCtx;
+
+#[inline]
+pub fn sim_update(ctx: &mut UpdateCtx) {
+ // if the water can fall, do so
+ if ctx.candidates_swap(&[
+ (0, -1),
+ (-1 + 2 * ctx.seqno_parity as i32, -1),
+ (1 - 2 * ctx.seqno_parity as i32, -1),
+ ]) {
+ return;
+ }
+
+ // if the water can't fall, check if we can move left or right
+ // these are inverted on parity so that we don't preference a direction
+ let left_target = ctx.get_cell(-1, 0);
+ let can_move_left =
+ left_target.is_some_and(|c| c.material.def().density < ctx.material.density);
+ let right_target = ctx.get_cell(1, 0);
+ let can_move_right =
+ right_target.is_some_and(|c| c.material.def().density < ctx.material.density);
+
+ // we can't move down or to other side, so we're stuck
+ if !can_move_left && !can_move_right {
+ return;
+ }
+
+ // if we can't move left, just move right
+ if !can_move_left {
+ ctx.candidates_swap(&[(1, 0)]);
+ return;
+ }
+ // and vice versa
+ if !can_move_right {
+ ctx.candidates_swap(&[(-1, 0)]);
+ return;
+ }
+
+ // find the closest hole within 20 pixels (TODO optimize)
+ // a hole is any space below us with a lesser density
+ // prevents equidistance stuck state
+ let starting_side = if ctx.seqno_parity == 0 { 1 } else { -1 };
+ for i in 0..20 {
+ let side = if i % 2 == 0 {
+ starting_side
+ } else {
+ -starting_side
+ };
+
+ let offset = side * (1 + i / 2);
+ let hole_target = ctx.get_cell(offset, -1);
+ if let Some(target) = hole_target
+ && target.material.def().density < ctx.material.density
+ {
+ // we identified a hole and we know that the space on this side is open
+ // move toward the hole
+ // new_target.flags = new_target.flags ^ 0b1;
+ ctx.candidates_swap(&[(side, 0)]);
+ return;
+ }
+ }
+
+ // we didn't find a hole, so just move "randomly" on the same surface
+ // TODO when to settle?
+ let target_x = if !can_move_left {
+ 1
+ } else if !can_move_right {
+ -1
+ } else if ctx.seqno_parity % 2 == 1 {
+ 1
+ } else {
+ -1
+ };
+
+ ctx.candidates_swap(&[(target_x, 0)]);
+}
diff --git a/src/sim/cell/materials/mod.rs b/src/sim/cell/materials/mod.rs
new file mode 100644
index 0000000..a55ed51
--- /dev/null
+++ b/src/sim/cell/materials/mod.rs
@@ -0,0 +1,88 @@
+use crate::sim::cell_sim::sim::UpdateCtx;
+
+mod fire;
+mod gas;
+mod sand;
+mod smoke;
+mod water;
+
+#[repr(u8)]
+#[derive(Clone, Copy, PartialEq, Eq, Debug)]
+pub enum MaterialId {
+ Void = 0,
+ Sand,
+ Wood,
+ Water,
+ Gas,
+ Fire,
+ Smoke,
+}
+
+pub struct MaterialDef {
+ pub name: &'static str,
+ pub color: (u8, u8, u8, u8),
+ pub density: u8,
+ pub sim_update: Option<fn(ctx: &mut UpdateCtx) -> ()>,
+}
+
+static MATERIALS: [MaterialDef; 7] = [
+ MaterialDef {
+ name: "Void",
+ color: (0x00, 0x00, 0x00, 0x00),
+ density: 0,
+ sim_update: None,
+ },
+ MaterialDef {
+ name: "Sand",
+ color: (0xDE, 0xCB, 0x85, 0xFF),
+ density: 50,
+ sim_update: Some(sand::sim_update),
+ },
+ MaterialDef {
+ name: "Wood",
+ color: (0x85, 0x56, 0x1D, 0xFF),
+ density: 50,
+ sim_update: None,
+ },
+ MaterialDef {
+ name: "Water",
+ color: (0x38, 0xA9, 0xFF, 0xFF),
+ density: 40,
+ sim_update: Some(water::sim_update),
+ },
+ MaterialDef {
+ name: "Gas",
+ color: (0xBD, 0xFF, 0xE4, 0xAA),
+ density: 10,
+ sim_update: Some(gas::sim_update),
+ },
+ MaterialDef {
+ name: "Fire",
+ color: (0xFC, 0x66, 0x00, 0xAA),
+ // for now this matches wood
+ density: 50,
+ sim_update: Some(fire::sim_update),
+ },
+ MaterialDef {
+ name: "Smoke",
+ color: (0x32, 0x35, 0x36, 0xAA),
+ density: 15,
+ sim_update: Some(smoke::sim_update),
+ },
+];
+
+impl MaterialId {
+ pub const ALL: [MaterialId; 7] = [
+ MaterialId::Void,
+ MaterialId::Sand,
+ MaterialId::Wood,
+ MaterialId::Water,
+ MaterialId::Gas,
+ MaterialId::Fire,
+ MaterialId::Smoke,
+ ];
+ #[inline]
+ pub fn def(self) -> &'static MaterialDef {
+ &MATERIALS[self as usize]
+ }
+}
diff --git a/src/sim/cell/materials/sand.rs b/src/sim/cell/materials/sand.rs
new file mode 100644
index 0000000..d2d1265
--- /dev/null
+++ b/src/sim/cell/materials/sand.rs
@@ -0,0 +1,10 @@
+use crate::sim::cell_sim::sim::UpdateCtx;
+
+#[inline]
+pub fn sim_update(ctx: &mut UpdateCtx) {
+ ctx.candidates_swap(&[
+ (0, 1),
+ (-1 + 2 * ctx.seqno_parity as i32, 1),
+ (1 - 2 * ctx.seqno_parity as i32, 1),
+ ]);
+}
diff --git a/src/sim/cell/materials/smoke.rs b/src/sim/cell/materials/smoke.rs
new file mode 100644
index 0000000..2ee7c00
--- /dev/null
+++ b/src/sim/cell/materials/smoke.rs
@@ -0,0 +1,20 @@
+use rand::RngExt;
+
+use crate::sim::cell_sim::sim::UpdateCtx;
+
+#[inline]
+pub fn sim_update(ctx: &mut UpdateCtx) {
+ // only allow upward movement some of the time to limit movement speed
+ if ctx.rng.random_range(0.0..1.0) > 0.8 && ctx.candidates_swap(&[(0, -1)]) {
+ return;
+ }
+ // same for each horizontal direction
+ if ctx.rng.random_range(0.0..1.0) > 0.8
+ && ctx.candidates_swap(&[(1 - ctx.seqno_parity as i32 * 2, 0)])
+ {
+ return;
+ }
+ if ctx.rng.random_range(0.0..1.0) > 0.8
+ && ctx.candidates_swap(&[(-1 + ctx.seqno_parity as i32 * 2, 0)])
+ {}
+}
diff --git a/src/sim/cell/materials/water.rs b/src/sim/cell/materials/water.rs
new file mode 100644
index 0000000..4b848fb
--- /dev/null
+++ b/src/sim/cell/materials/water.rs
@@ -0,0 +1,76 @@
+use crate::sim::cell_sim::sim::UpdateCtx;
+
+#[inline]
+pub fn sim_update(ctx: &mut UpdateCtx) {
+ // if the water can fall, do so
+ if ctx.candidates_swap(&[
+ (0, 1),
+ (-1 + 2 * ctx.seqno_parity as i32, 1),
+ (1 - 2 * ctx.seqno_parity as i32, 1),
+ ]) {
+ return;
+ }
+
+ // if the water can't fall, check if we can move left or right
+ // these are inverted on parity so that we don't preference a direction
+ let left_target = ctx.get_cell(-1, 0);
+ let can_move_left =
+ left_target.is_some_and(|c| c.material.def().density < ctx.material.density);
+ let right_target = ctx.get_cell(1, 0);
+ let can_move_right =
+ right_target.is_some_and(|c| c.material.def().density < ctx.material.density);
+
+ // we can't move down or to other side, so we're stuck
+ if !can_move_left && !can_move_right {
+ return;
+ }
+
+ // if we can't move left, just move right
+ if !can_move_left {
+ ctx.candidates_swap(&[(1, 0)]);
+ return;
+ }
+ // and vice versa
+ if !can_move_right {
+ ctx.candidates_swap(&[(-1, 0)]);
+ return;
+ }
+
+ // find the closest hole within 20 pixels (TODO optimize)
+ // a hole is any space below us with a lesser density
+ // prevents equidistance stuck state
+ let starting_side = if ctx.seqno_parity == 0 { 1 } else { -1 };
+ for i in 0..20 {
+ let side = if i % 2 == 0 {
+ starting_side
+ } else {
+ -starting_side
+ };
+
+ let offset = side * (1 + i / 2);
+ let hole_target = ctx.get_cell(offset, 1);
+ if let Some(target) = hole_target
+ && target.material.def().density < ctx.material.density
+ {
+ // we identified a hole and we know that the space on this side is open
+ // move toward the hole
+ // new_target.flags = new_target.flags ^ 0b1;
+ ctx.candidates_swap(&[(side, 0)]);
+ return;
+ }
+ }
+
+ // we didn't find a hole, so just move "randomly" on the same surface
+ // TODO when to settle?
+ // let target_x = if !can_move_left {
+ // 1
+ // } else if !can_move_right {
+ // -1
+ // } else if ctx.seqno_parity % 2 == 1 {
+ // 1
+ // } else {
+ // -1
+ // };
+
+ // ctx.candidates_swap(&[(target_x, 0)]);
+}
diff --git a/src/sim/cell/mod.rs b/src/sim/cell/mod.rs
new file mode 100644
index 0000000..2d2175c
--- /dev/null
+++ b/src/sim/cell/mod.rs
@@ -0,0 +1,2 @@
+pub mod cell;
+pub mod materials;