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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ unit-testable with plain `cargo test`.
| Steam | Rises; condenses when cool; water boils next to fire |
| Fan | Static; click the swatch to cycle facing; blows gases in that 4×4 |
| Gunpowder | Falls like sand; explodes from fire or heat 80 |
| Smoke | Rises slower than steam; leftover from burned-out fire |
| Erase / Clear | Remove cells |

## Quickstart
Expand Down
3 changes: 3 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ <h1>Sandboxed</h1>
<button data-mat="11">
<span class="swatch" style="background: #28241e"></span>Gunpowder
</button>
<button data-mat="12">
<span class="swatch" style="background: #606064"></span>Smoke
</button>
<button data-mat="0">
<span class="swatch" style="background: #1a1a24"></span>Erase
</button>
Expand Down
2 changes: 1 addition & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ impl Simulation {
}

/// Paint `material` (0=erase, 1=sand, 2=water, 3=stone, 4=fire, 5=wood,
/// 6=steam, 7=fan→, 8=fan←, 9=fan↑, 10=fan↓, 11=gunpowder) at (x, y).
/// 6=steam, 7=fan→, 8=fan←, 9=fan↑, 10=fan↓, 11=gunpowder, 12=smoke) 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
4 changes: 4 additions & 0 deletions src/renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,10 @@ pub fn draw(world: &World, frame: &mut [u8]) {
let (r, g, b) = warm(40 + j / 2, 36 + j / 2, 32, h);
put(frame, i, r, g, b);
}
Cell::Smoke => {
let (r, g, b) = warm(96 + j / 2, 96 + j / 2, 100, h);
put(frame, i, r, g, b);
}
}
}
}
Expand Down
117 changes: 113 additions & 4 deletions src/world.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pub enum Cell {
FanUp = 9,
FanDown = 10,
Gunpowder = 11,
Smoke = 12,
}

impl Cell {
Expand All @@ -36,6 +37,7 @@ impl Cell {
9 => Some(Cell::FanUp),
10 => Some(Cell::FanDown),
11 => Some(Cell::Gunpowder),
12 => Some(Cell::Smoke),
_ => None,
}
}
Expand All @@ -46,7 +48,7 @@ impl Cell {
Cell::Empty => 0,
Cell::Water => 1,
Cell::Gunpowder | Cell::Sand => 2,
Cell::Fire | Cell::Steam => 0,
Cell::Fire | Cell::Steam | Cell::Smoke => 0,
Cell::Stone
| Cell::Wood
| Cell::FanRight
Expand Down Expand Up @@ -82,6 +84,8 @@ const WOOD_IGNITE: u8 = 80;
const WATER_BOIL: u8 = 100;
const STEAM_CONDENSE: u8 = 40;
const STEAM_PAINT_HEAT: u8 = 120;
const SMOKE_PAINT_HEAT: u8 = 80;
const SMOKE_DISPERSE: u8 = 10;
const CHUNK: usize = 16;
pub(crate) const AIR_CELL: usize = 4;
const FAN_SPEED: i16 = 40;
Expand Down Expand Up @@ -505,6 +509,7 @@ impl World {
self.heat[i] = match cell {
Cell::Fire => FIRE_HEAT,
Cell::Steam => STEAM_PAINT_HEAT,
Cell::Smoke => SMOKE_PAINT_HEAT,
_ => 0,
};
self.wake(px, py, false);
Expand Down Expand Up @@ -634,6 +639,7 @@ impl World {
Cell::Water => self.step_water(x, y),
Cell::Fire => self.step_fire(x, y),
Cell::Steam => self.step_steam(x, y),
Cell::Smoke => self.step_smoke(x, y),
_ => {}
}
}
Expand Down Expand Up @@ -763,6 +769,13 @@ impl World {
self.wake(x, y, true);
}

fn become_smoke(&mut self, x: usize, y: usize) {
let i = self.idx(x, y);
self.cells[i] = Cell::Smoke;
self.ttl[i] = 0;
self.wake(x, y, true);
}

fn step_fire(&mut self, x: usize, y: usize) {
self.wake(x, y, true);
if let Some((wx, wy)) = self.first_cardinal(x, y, Cell::Water) {
Expand All @@ -777,12 +790,12 @@ impl World {
self.conduct(x, y);
let life = self.ttl[i];
if life <= 1 {
self.extinguish(x, y);
self.become_smoke(x, y);
return;
}
// Flicker: extra chance to die near the end of life.
if life <= 4 && self.next_rand() & 7 == 0 {
self.extinguish(x, y);
self.become_smoke(x, y);
return;
}
self.ttl[i] = life - 1;
Expand Down Expand Up @@ -853,6 +866,49 @@ impl World {
}
}

fn step_smoke(&mut self, x: usize, y: usize) {
self.wake(x, y, true);
let i = self.idx(x, y);
self.heat[i] = self.heat[i].saturating_sub(1);
self.conduct(x, y);
let i = self.idx(x, y);
if self.cells[i] != Cell::Smoke {
return;
}
if self.heat[i] < SMOKE_DISPERSE {
self.cells[i] = Cell::Empty;
self.heat[i] = 0;
self.ttl[i] = 0;
self.wake(x, y, true);
return;
}
if self.try_air_push(x, y, GAS_PUSH) {
return;
}
if !self.frame.is_multiple_of(2) {
return;
}
if y == 0 {
return;
}
let above = y - 1;
if self.try_move(x, y, x, above) {
return;
}
let left_first = self.next_rand() & 1 == 0;
let diagonals: [(isize, usize); 2] = [(-1, above), (1, above)];
for k in 0..2 {
let (dx, ny) = diagonals[if left_first { k } else { 1 - k }];
let nx = x as isize + dx;
if nx < 0 {
continue;
}
if self.try_move(x, y, nx as usize, ny) {
return;
}
}
}

fn try_air_push(&mut self, x: usize, y: usize, threshold: i16) -> bool {
let i = self.air_idx_of(x, y);
let vx = self.vx[i];
Expand Down Expand Up @@ -1181,6 +1237,58 @@ mod tests {
assert_eq!(count(&w, Cell::Fire), 0);
}

#[test]
fn fire_burns_to_smoke_then_empty() {
let mut w = World::new(8, 8);
w.paint(4, 3, Cell::Fire, 0);
let mut saw_smoke = false;
for _ in 0..400 {
w.step();
if count(&w, Cell::Smoke) > 0 {
saw_smoke = true;
}
if saw_smoke && count(&w, Cell::Fire) == 0 && count(&w, Cell::Smoke) == 0 {
break;
}
}
assert!(saw_smoke, "isolated fire should leave smoke");
assert_eq!(count(&w, Cell::Fire), 0);
assert_eq!(count(&w, Cell::Smoke), 0);
}

#[test]
fn water_quench_does_not_spawn_smoke() {
let mut w = World::new(8, 8);
w.paint(3, 4, Cell::Fire, 0);
w.paint(4, 4, Cell::Water, 0);
for _ in 0..16 {
w.step();
assert_eq!(count(&w, Cell::Smoke), 0);
}
}

#[test]
fn smoke_rises_slower_than_steam() {
let mut steam = World::new(8, 16);
let mut smoke = World::new(8, 16);
steam.paint(4, 14, Cell::Steam, 0);
smoke.paint(4, 14, Cell::Smoke, 0);
for _ in 0..8 {
steam.step();
smoke.step();
}
let steam_y = (0..16)
.find(|&y| (0..8).any(|x| steam.get(x, y) == Cell::Steam))
.expect("steam still present");
let smoke_y = (0..16)
.find(|&y| (0..8).any(|x| smoke.get(x, y) == Cell::Smoke))
.expect("smoke still present");
assert!(
smoke_y > steam_y,
"smoke should lag steam: smoke_y={smoke_y} steam_y={steam_y}"
);
}

#[test]
fn fire_rises() {
let mut w = World::new(8, 8);
Expand Down Expand Up @@ -1445,7 +1553,8 @@ mod tests {
assert_eq!(Cell::from_u8(9), Some(Cell::FanUp));
assert_eq!(Cell::from_u8(10), Some(Cell::FanDown));
assert_eq!(Cell::from_u8(11), Some(Cell::Gunpowder));
assert_eq!(Cell::from_u8(12), None);
assert_eq!(Cell::from_u8(12), Some(Cell::Smoke));
assert_eq!(Cell::from_u8(13), None);
}

#[test]
Expand Down
Loading