Day 14: Code
Below is the complete code explanation for Day 14's solution, which simulates falling sand in a cave system with rock formations.
Code Structure
The solution is quite extensive and uses several key components:
- A
Board<T>
struct to represent the cave grid - A
Material
enum for different types of material (rock, sand, air) - A
Grain
struct to track individual sand units - A
Painter
helper to draw rock formations - Simulation logic for falling sand
- Visualization components using bracket-lib
Key Components
Board and Materials
The cave is represented by a Board
struct with a hashmap grid:
struct Board<T> {
width: usize,
height: usize,
centre_x: usize,
offset_x: usize,
grid: HashMap<Coord,T>,
}
The materials in the cave are represented by an enum:
enum Material { Rock, Sand, Air }
impl Default for Material {
fn default() -> Self { Material::Air }
}
Sand Grain Representation
Each unit of sand is represented by a Grain
struct:
struct Grain {
pos: Coord,
settled: bool
}
Parsing Rock Formations
The input is parsed into rock formations:
fn parse_plines(input:&str) -> (Coord, Coord, Vec<Vec<Coord>>) {
let mut br = Coord{ x: usize::MIN, y: usize::MIN };
let mut tl = Coord{ x: usize::MAX, y: 0 };
let plines =
input.lines()
.map(|line|{
line.split(" -> ")
.map(|val| Coord::from_str(val).expect("Ops!"))
.inspect(|p|{
tl.x = std::cmp::min(tl.x, p.x);
br.x = std::cmp::max(br.x, p.x);
br.y = std::cmp::max(br.y, p.y);
})
.collect::<Vec<_>>()
})
.fold(vec![],|mut out, pline|{
out.push(pline);
out
});
(tl, br, plines)
}
Drawing Rock Walls
Rock walls are drawn between consecutive points:
fn rock_walls(board: &mut Board<Material>, c: &[Coord]) {
c.windows(2)
.for_each(| p|
Painter::wall(board, p[0], p[1], Material::Rock)
);
}
Sand Movement Simulation
The core of the solution is the sand movement logic:
fn fall(&mut self, board: &Board<Material>) -> Option<Coord> {
if self.settled { return None }
let Coord{ x, y} = self.pos;
let [lc, uc, rc] = [(x-1, y+1).into(), (x, y+1).into(), (x+1, y+1).into()];
let l = board.square( lc );
let u = board.square( uc );
let r = board.square( rc );
match (l,u,r) {
(_, None, _) => None,
(_, Some(Material::Air), _) => { self.pos = uc; Some(self.pos) },
(Some(Material::Air), _, _) => { self.pos = lc; Some(self.pos) },
(_, _, Some(Material::Air)) => { self.pos = rc; Some(self.pos) },
(_, _, _) => { self.settled = true; None }
}
}
fn is_settled(&self) -> bool {
self.settled
}
}
Running the Simulation
The simulation runs until a specified condition is met:
fn run<F>(&mut self, start: Coord, check_goal: F) where F: Fn(&Grain) -> bool {
loop {
let mut grain = Grain::release_grain(start);
// let the grain fall until it either (a) settles or (b) falls off the board
while grain.fall(self).is_some() {};
// Have we reached an end state ?
// we use a closure that passes the stopped grain
// for checking whether (a) it has fallen in the abyss or (b) reached the starting position
if check_goal(&grain) {
// Mark settled grain position on the board
*self.square_mut(grain.pos).unwrap() = Material::Sand;
break
}
// Mark settled grain position on the board
*self.square_mut(grain.pos).unwrap() = Material::Sand;
}
}
Managing the Floor (Part 2)
A floor is added for Part 2:
fn toggle_floor(&mut self) {
let height = self.height-1;
let left = Coord { x: self.offset_x, y: height };
let right = Coord { x: self.offset_x + self.width - 1, y : height };
match self.square(left) {
Some(Material::Rock) => Painter::wall(self, left, right, Material::Air),
_ => Painter::wall(self, left, right, Material::Rock)
}
}
Counting Sand Grains
The solution counts sand grains at rest:
fn grains_at_rest(&self) -> usize {
self.grid.values()
.filter(|&s| Material::Sand.eq(s) )
.count()
}
Main Function
The main function sets up the simulation and runs both parts of the problem:
fn main() -> BResult<()> {
// let input = "498,4 -> 498,6 -> 496,6\n503,4 -> 502,4 -> 502,9 -> 494,9".to_string();
let input = std::fs::read_to_string("src/bin/day14_input.txt").expect("ops!");
// parse the board's wall layout
let (tl, br, plines) = parse_plines(input.as_str());
let mut board = Board::new(tl, br);
// paint layout on the board
plines.into_iter()
.for_each(|pline|
Painter::rock_walls(&mut board, &pline)
);
// run the sand simulation until we reach the abyss, that is, grain stopped but not settled
let start = (board.centre_x, 0).into();
board.run(
start, |g| !g.is_settled()
);
println!("Scenario 1: Grains Rest: {}\n{:?}", board.grains_at_rest() - 1, board);
board.empty_sand();
// add rock floor
board.toggle_floor();
// run the sand simulation until grain settled position == starting position
board.run(
start, |g| g.pos.eq(&start)
);
println!("Scenario 2: Grains Rest: {}\n{:?}", board.grains_at_rest(), board);
Visualization
The solution includes a visualization component using bracket-lib:
let ctx = BTermBuilder::simple(board.width >> 1, board.height >> 1)?
.with_simple_console(board.width, board.height, "terminal8x8.png")
.with_simple_console_no_bg(board.width, board.height, "terminal8x8.png")
.with_simple_console_no_bg(board.width >> 2, board.height >> 2, "terminal8x8.png")
.with_fps_cap(60f32)
.with_title("S: Reset, R: Run, G: Grain: Q: Quit")
.build()?;
let mut app = App::init(
Store {
board,
grains: VecDeque::new(),
start
},
Levels::MENU
);
app.register_level(Levels::MENU, Menu);
app.register_level(Levels::LEVEL1, ExerciseOne {run:false, abyss:false} );
app.register_level(Levels::LEVEL2, ExerciseTwo {ceiling:false} );
main_loop(ctx, app)
Implementation Notes
- Grid Representation: The solution uses a hashmap for the grid, which is memory-efficient for sparse grids
- Flexible Simulation: The
run
method takes a closure parameter to allow different stopping conditions - Visualization: The solution includes a real-time visualization of the falling sand
- Movement Logic: Sand follows specific rules with a priority order of movement directions
The code elegantly handles both parts of the problem using a comprehensive simulation of the physical process described in the problem.