From e4109c5386ced804455f85fa4d919df12acd6e21 Mon Sep 17 00:00:00 2001 From: chowjiaming Date: Thu, 13 Aug 2026 16:26:09 -0400 Subject: [PATCH] feat: add flammable wood that burns from adjacent fire Wood is static like stone. Fire ignites cardinal wood neighbors with p=0.1 per tick and stays put while fuel remains, so a flame does not float off a wall before anything catches. --- README.md | 3 +- index.html | 1 + src/lib.rs | 2 +- src/renderer.rs | 1 + src/world.rs | 87 +++++++++++++++++++++++++++++++++++++++++++++++-- 5 files changed, 89 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 679fc85..4a1252b 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ WebAssembly, and rendered in the browser. **[Live demo](https://chowjiaming.github.io/sandboxed/)** -Paint sand, water, stone, and fire onto a 320×200 grid. The +Paint sand, water, stone, fire, and wood onto a 320×200 grid. The simulation runs in WASM: a flat `Vec`, no per-frame allocation, and a thin `wasm-bindgen` boundary so the core stays unit-testable with plain `cargo test`. @@ -28,6 +28,7 @@ unit-testable with plain `cargo test`. | Water | Flows and spreads | | Stone | Static obstacle | | Fire | Rises, flickers, burns out | +| Wood | Static; ignites from adjacent fire | | Erase / Clear | Remove cells | ## Quickstart diff --git a/index.html b/index.html index b7261df..035a1f5 100644 --- a/index.html +++ b/index.html @@ -70,6 +70,7 @@

Sandboxed

+ diff --git a/src/lib.rs b/src/lib.rs index a0214d7..2c942cb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -44,7 +44,7 @@ impl Simulation { self.world.height } - /// Paint `material` (0=erase, 1=sand, 2=water, 3=stone, 4=fire) at (x, y). + /// Paint `material` (0=erase, 1=sand, 2=water, 3=stone, 4=fire, 5=wood) at (x, y). pub fn paint(&mut self, x: usize, y: usize, material: u8, radius: usize) { if let Some(cell) = Cell::from_u8(material) { self.world.paint(x, y, cell, radius); diff --git a/src/renderer.rs b/src/renderer.rs index 9b0936b..72b52d1 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -34,6 +34,7 @@ pub fn draw(world: &World, frame: &mut [u8]) { let t = world.ttl()[i] as i16; put(frame, i, 255, 32 + t * 4, 16); } + Cell::Wood => put(frame, i, 118 + j / 2, 72 + j / 2, 36), } } } diff --git a/src/world.rs b/src/world.rs index 1e5ddef..7ae8116 100644 --- a/src/world.rs +++ b/src/world.rs @@ -12,6 +12,7 @@ pub enum Cell { Water = 2, Stone = 3, Fire = 4, + Wood = 5, } impl Cell { @@ -22,6 +23,7 @@ impl Cell { 2 => Some(Cell::Water), 3 => Some(Cell::Stone), 4 => Some(Cell::Fire), + 5 => Some(Cell::Wood), _ => None, } } @@ -33,7 +35,7 @@ impl Cell { Cell::Water => 1, Cell::Sand => 2, Cell::Fire => 0, - Cell::Stone => 255, // immovable + Cell::Stone | Cell::Wood => 255, // immovable } } } @@ -114,9 +116,9 @@ impl World { if px >= self.width || py >= self.height { continue; } - // Don't overwrite stone with loose material (eraser can). + // Don't overwrite stone or wood with loose material (eraser can). let existing = self.get(px, py); - if existing == Cell::Stone && cell != Cell::Empty { + if matches!(existing, Cell::Stone | Cell::Wood) && cell != Cell::Empty { continue; } let i = self.idx(px, py); @@ -171,6 +173,54 @@ impl World { } } + fn ignite(&mut self, x: usize, y: usize) { + let i = self.idx(x, y); + self.cells[i] = Cell::Fire; + self.ttl[i] = FIRE_LIFETIME; + self.moved[i] = true; + } + + fn has_adjacent_wood(&self, x: usize, y: usize) -> bool { + self.for_each_cardinal(x, y, |cell| cell == Cell::Wood) + } + + fn try_ignite_neighbors(&mut self, x: usize, y: usize) { + const DIRS: [(isize, isize); 4] = [(0, -1), (0, 1), (-1, 0), (1, 0)]; + for (dx, dy) in DIRS { + let nx = x as isize + dx; + let ny = y as isize + dy; + if nx < 0 || ny < 0 { + continue; + } + let (nx, ny) = (nx as usize, ny as usize); + if nx >= self.width || ny >= self.height { + continue; + } + if self.get(nx, ny) == Cell::Wood && self.next_rand().is_multiple_of(10) { + self.ignite(nx, ny); + } + } + } + + fn for_each_cardinal(&self, x: usize, y: usize, mut pred: impl FnMut(Cell) -> bool) -> bool { + const DIRS: [(isize, isize); 4] = [(0, -1), (0, 1), (-1, 0), (1, 0)]; + for (dx, dy) in DIRS { + let nx = x as isize + dx; + let ny = y as isize + dy; + if nx < 0 || ny < 0 { + continue; + } + let (nx, ny) = (nx as usize, ny as usize); + if nx >= self.width || ny >= self.height { + continue; + } + if pred(self.get(nx, ny)) { + return true; + } + } + false + } + fn extinguish(&mut self, x: usize, y: usize) { let i = self.idx(x, y); self.cells[i] = Cell::Empty; @@ -178,6 +228,7 @@ impl World { } fn step_fire(&mut self, x: usize, y: usize) { + self.try_ignite_neighbors(x, y); let i = self.idx(x, y); let life = self.ttl[i]; if life <= 1 { @@ -190,6 +241,10 @@ impl World { return; } self.ttl[i] = life - 1; + // Stay put while there is fuel so a flame doesn't float off a wall. + if self.has_adjacent_wood(x, y) { + return; + } if y == 0 { return; } @@ -469,4 +524,30 @@ mod tests { assert_eq!(w.get(4, 4), Cell::Fire); assert_eq!(w.get(4, 5), Cell::Empty); } + + #[test] + fn wood_is_static() { + let mut w = World::new(8, 8); + w.paint(4, 2, Cell::Wood, 0); + for _ in 0..16 { + w.step(); + } + assert_eq!(w.get(4, 2), Cell::Wood); + } + + #[test] + fn wood_block_burns_within_n_ticks() { + let mut w = World::new(8, 8); + for x in 3..=5 { + for y in 3..=5 { + w.paint(x, y, Cell::Wood, 0); + } + } + w.paint(2, 4, Cell::Fire, 0); + assert_eq!(count(&w, Cell::Wood), 9); + for _ in 0..2000 { + w.step(); + } + assert_eq!(count(&w, Cell::Wood), 0, "wood block did not fully burn"); + } }