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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ unit-testable with plain `cargo test`.
| 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 |
| Oil | Floats on water; ignites from fire or heat 60 |
| Ice | Static; melts to water at heat 25; water freezes below 15 |
| Erase / Clear | Remove cells |

## Quickstart
Expand Down
6 changes: 6 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,12 @@ <h1>Sandboxed</h1>
<button data-mat="12">
<span class="swatch" style="background: #606064"></span>Smoke
</button>
<button data-mat="13">
<span class="swatch" style="background: #b46e18"></span>Oil
</button>
<button data-mat="14">
<span class="swatch" style="background: #b4dcf0"></span>Ice
</button>
<button data-mat="0">
<span class="swatch" style="background: #1a1a24"></span>Erase
</button>
Expand Down
3 changes: 2 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@ 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, 12=smoke) at (x, y).
/// 6=steam, 7=fan→, 8=fan←, 9=fan↑, 10=fan↓, 11=gunpowder, 12=smoke,
/// 13=oil, 14=ice) 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
8 changes: 8 additions & 0 deletions src/renderer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,14 @@ pub fn draw(world: &World, frame: &mut [u8]) {
let (r, g, b) = warm(96 + j / 2, 96 + j / 2, 100, h);
put(frame, i, r, g, b);
}
Cell::Oil => {
let (r, g, b) = warm(180 + j / 2, 110 + j / 3, 24, h);
put(frame, i, r, g, b);
}
Cell::Ice => {
let (r, g, b) = warm(180 + j / 3, 220 + j / 2, 240, h);
put(frame, i, r, g, b);
}
}
}
}
Expand Down
134 changes: 124 additions & 10 deletions src/world.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ pub enum Cell {
FanDown = 10,
Gunpowder = 11,
Smoke = 12,
Oil = 13,
Ice = 14,
}

impl Cell {
Expand All @@ -38,19 +40,22 @@ impl Cell {
10 => Some(Cell::FanDown),
11 => Some(Cell::Gunpowder),
12 => Some(Cell::Smoke),
13 => Some(Cell::Oil),
14 => Some(Cell::Ice),
_ => None,
}
}

/// "Density" used for sinking behavior: sand sinks through water.
fn density(self) -> u8 {
match self {
Cell::Empty => 0,
Cell::Water => 1,
Cell::Gunpowder | Cell::Sand => 2,
Cell::Fire | Cell::Steam | Cell::Smoke => 0,
Cell::Empty | Cell::Fire | Cell::Steam | Cell::Smoke => 0,
Cell::Oil => 1,
Cell::Water => 2,
Cell::Gunpowder | Cell::Sand => 3,
Cell::Stone
| Cell::Wood
| Cell::Ice
| Cell::FanRight
| Cell::FanLeft
| Cell::FanUp
Expand All @@ -68,12 +73,12 @@ impl Cell {

#[inline]
fn is_locked(self) -> bool {
matches!(self, Cell::Stone | Cell::Wood) || self.is_fan()
matches!(self, Cell::Stone | Cell::Wood | Cell::Ice) || self.is_fan()
}

#[inline]
fn blocks_air(self) -> bool {
matches!(self, Cell::Stone | Cell::Wood)
matches!(self, Cell::Stone | Cell::Wood | Cell::Ice)
}
}

Expand All @@ -86,6 +91,10 @@ const STEAM_CONDENSE: u8 = 40;
const STEAM_PAINT_HEAT: u8 = 120;
const SMOKE_PAINT_HEAT: u8 = 80;
const SMOKE_DISPERSE: u8 = 10;
const ROOM_HEAT: u8 = 50;
const OIL_IGNITE: u8 = 60;
const ICE_FREEZE: u8 = 15;
const ICE_MELT: u8 = 25;
const CHUNK: usize = 16;
pub(crate) const AIR_CELL: usize = 4;
const FAN_SPEED: i16 = 40;
Expand Down Expand Up @@ -510,6 +519,7 @@ impl World {
Cell::Fire => FIRE_HEAT,
Cell::Steam => STEAM_PAINT_HEAT,
Cell::Smoke => SMOKE_PAINT_HEAT,
Cell::Water | Cell::Oil => ROOM_HEAT,
_ => 0,
};
self.wake(px, py, false);
Expand Down Expand Up @@ -637,6 +647,7 @@ impl World {
Cell::Sand => self.step_sand(x, y),
Cell::Gunpowder => self.step_gunpowder(x, y),
Cell::Water => self.step_water(x, y),
Cell::Oil => self.step_oil(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 @@ -678,11 +689,16 @@ impl World {
self.wake(x, y, true);
match self.cells[i] {
Cell::Wood if self.heat[i] >= WOOD_IGNITE => self.ignite(x, y),
Cell::Oil if self.heat[i] >= OIL_IGNITE => self.ignite(x, y),
Cell::Water if self.heat[i] >= WATER_BOIL => {
self.cells[i] = Cell::Steam;
self.ttl[i] = 0;
self.add_pv(x, y, BOIL_PV);
}
Cell::Ice if self.heat[i] >= ICE_MELT => {
self.cells[i] = Cell::Water;
self.ttl[i] = 0;
}
_ => {}
}
}
Expand All @@ -705,7 +721,8 @@ impl World {
if nx >= self.width || ny >= self.height {
continue;
}
if self.get(nx, ny) == Cell::Empty {
let n = self.get(nx, ny);
if n == Cell::Empty || n == Cell::Stone || n.is_fan() {
continue;
}
let ni = self.idx(nx, ny);
Expand Down Expand Up @@ -982,7 +999,7 @@ impl World {
continue;
}
let cell = self.get(nx, ny);
if cell == Cell::Stone || cell.is_fan() {
if cell == Cell::Stone || cell == Cell::Ice || cell.is_fan() {
continue;
}
self.ignite(nx, ny);
Expand Down Expand Up @@ -1018,18 +1035,43 @@ impl World {
return;
}
}
let i = self.idx(x, y);
if self.heat[i] < ICE_FREEZE {
self.cells[i] = Cell::Ice;
self.ttl[i] = 0;
self.wake(x, y, true);
return;
}
if let Some((fx, fy)) = self.first_cardinal(x, y, Cell::Fire) {
let bump = WATER_BOIL.saturating_sub(self.heat[i]);
self.add_heat(x, y, bump);
self.extinguish(fx, fy);
return;
}
self.slide_liquid(x, y);
}

fn step_oil(&mut self, x: usize, y: usize) {
let i = self.idx(x, y);
if self.heat[i] > 0 {
self.conduct(x, y);
if self.get(x, y) != Cell::Oil {
return;
}
}
if self.heat[i] >= OIL_IGNITE || self.first_cardinal(x, y, Cell::Fire).is_some() {
self.ignite(x, y);
return;
}
self.slide_liquid(x, y);
}

fn slide_liquid(&mut self, x: usize, y: usize) {
let below = y + 1;
if below < self.height && self.try_move(x, y, x, below) {
return;
}
let left_first = self.next_rand() & 1 == 0;
// Diagonals, then horizontal slide (water spreads).
let moves: [(isize, isize); 4] = [(-1, 1), (1, 1), (-1, 0), (1, 0)];
for k in 0..4 {
let (dx, dy) = moves[if left_first { k } else { 3 - k }];
Expand Down Expand Up @@ -1554,7 +1596,9 @@ mod tests {
assert_eq!(Cell::from_u8(10), Some(Cell::FanDown));
assert_eq!(Cell::from_u8(11), Some(Cell::Gunpowder));
assert_eq!(Cell::from_u8(12), Some(Cell::Smoke));
assert_eq!(Cell::from_u8(13), None);
assert_eq!(Cell::from_u8(13), Some(Cell::Oil));
assert_eq!(Cell::from_u8(14), Some(Cell::Ice));
assert_eq!(Cell::from_u8(15), None);
}

#[test]
Expand All @@ -1567,6 +1611,76 @@ mod tests {
assert_eq!(w.get(1, 1), Cell::Empty);
}

#[test]
fn paint_cannot_overwrite_ice_except_eraser() {
let mut w = World::new(4, 4);
w.paint(1, 1, Cell::Ice, 0);
w.paint(1, 1, Cell::Sand, 0);
assert_eq!(w.get(1, 1), Cell::Ice);
w.paint(1, 1, Cell::Empty, 0);
assert_eq!(w.get(1, 1), Cell::Empty);
}

#[test]
fn oil_floats_on_water() {
let mut w = World::new(4, 8);
for y in 0..8 {
w.paint(1, y, Cell::Stone, 0);
w.paint(3, y, Cell::Stone, 0);
}
w.paint(2, 0, Cell::Oil, 0);
w.paint(2, 1, Cell::Water, 0);
for _ in 0..16 {
w.step();
}
let oil_y = (0..8)
.find(|&y| (0..4).any(|x| w.get(x, y) == Cell::Oil))
.expect("oil");
let water_y = (0..8)
.find(|&y| (0..4).any(|x| w.get(x, y) == Cell::Water))
.expect("water");
assert!(
oil_y < water_y,
"oil should rest above water: oil_y={oil_y} water_y={water_y}"
);
}

#[test]
fn oil_ignites_from_fire() {
let mut w = World::new(8, 8);
w.paint(4, 4, Cell::Fire, 0);
w.paint(5, 4, Cell::Oil, 0);
w.step();
assert_eq!(count(&w, Cell::Oil), 0);
assert!(count(&w, Cell::Fire) > 0);
}

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

#[test]
fn ice_melts_from_adjacent_fire() {
let mut w = World::new(8, 8);
w.paint(4, 3, Cell::Ice, 0);
w.paint(4, 4, Cell::Fire, 0);
for _ in 0..32 {
w.step();
if count(&w, Cell::Ice) == 0 {
break;
}
}
assert_eq!(count(&w, Cell::Ice), 0);
assert!(count(&w, Cell::Water) + count(&w, Cell::Steam) > 0);
}

#[test]
fn air_buffers_match_grid() {
let w = World::new(320, 200);
Expand Down
Loading