Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<Cell>`, no per-frame
allocation, and a thin `wasm-bindgen` boundary so the core stays
unit-testable with plain `cargo test`.
Expand All @@ -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
Expand Down
1 change: 1 addition & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ <h1>Sandboxed</h1>
<button data-mat="2">Water</button>
<button data-mat="3">Stone</button>
<button data-mat="4">Fire</button>
<button data-mat="5">Wood</button>
<button data-mat="0">Erase</button>
<button id="clear">Clear</button>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions src/renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
}
}
Expand Down
87 changes: 84 additions & 3 deletions src/world.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub enum Cell {
Water = 2,
Stone = 3,
Fire = 4,
Wood = 5,
}

impl Cell {
Expand All @@ -22,6 +23,7 @@ impl Cell {
2 => Some(Cell::Water),
3 => Some(Cell::Stone),
4 => Some(Cell::Fire),
5 => Some(Cell::Wood),
_ => None,
}
}
Expand All @@ -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
}
}
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -171,13 +173,62 @@ 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;
self.ttl[i] = 0;
}

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 {
Expand All @@ -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;
}
Expand Down Expand Up @@ -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");
}
}
Loading