advent-of-code 2025.5.0

Solutions to Advent of Code
Documentation
use crate::input::Input;
use std::collections::HashMap;
use std::collections::hash_map::{DefaultHasher, Entry};
use std::hash::{Hash, Hasher};
use std::mem::swap;

struct Grid {
    width: usize,
    height: usize,
    cells: Vec<u8>,
    next_gen_cells: Vec<u8>,
}

impl Grid {
    fn parse(input_string: &str) -> Result<Self, String> {
        let mut height = 0;
        let mut width = 0;
        let mut cells = Vec::new();
        for (line_idx, line) in input_string.lines().enumerate() {
            let new_width = line.len();
            if line_idx == 0 {
                width = new_width;
            } else if new_width != width {
                return Err("Not all lines have equal length".into());
            }
            line.chars().for_each(|c| cells.push(c as u8));
            height += 1;
        }

        if width == 0 {
            return Err("Empty input".into());
        }

        let next_gen_cells = vec![0; height * width];
        Ok(Self {
            width,
            height,
            cells,
            next_gen_cells,
        })
    }

    fn count_around(&self, x: usize, y: usize, needle: u8) -> u8 {
        let mut sum = 0;
        for dy in [-1_isize, 0, 1] {
            for dx in [-1_isize, 0, 1] {
                if dx == 0 && dy == 0 {
                    continue;
                }
                let nx = x.wrapping_add_signed(dx);
                let ny = y.wrapping_add_signed(dy);
                if nx < self.width && ny < self.height && self.cells[self.width * ny + nx] == needle
                {
                    sum += 1;
                }
            }
        }
        sum
    }

    fn advance_minute(&mut self) -> Result<(), String> {
        for y in 0..self.height {
            for x in 0..self.width {
                let cell_value = self.cells[self.width * y + x];
                self.next_gen_cells[self.width * y + x] = match cell_value {
                    b'.' => {
                        if self.count_around(x, y, b'|') >= 3 {
                            b'|'
                        } else {
                            b'.'
                        }
                    }
                    b'|' => {
                        if self.count_around(x, y, b'#') >= 3 {
                            b'#'
                        } else {
                            b'|'
                        }
                    }
                    b'#' => {
                        if self.count_around(x, y, b'#') >= 1 && self.count_around(x, y, b'|') >= 1
                        {
                            b'#'
                        } else {
                            b'.'
                        }
                    }
                    _ => {
                        return Err(format!("Unhandled cell value: {cell_value}"));
                    }
                }
            }
        }
        swap(&mut self.cells, &mut self.next_gen_cells);
        Ok(())
    }

    fn resource_value(&self) -> usize {
        self.cells
            .iter()
            .fold(0, |n, c| n + usize::from(*c == b'|'))
            * self
                .cells
                .iter()
                .fold(0, |n, c| n + usize::from(*c == b'#'))
    }
}

pub fn solve(input: &Input) -> Result<usize, String> {
    let mut grid = Grid::parse(input.text)?;

    if input.is_part_one() {
        for _ in 0..10 {
            grid.advance_minute()?;
        }
        Ok(grid.resource_value())
    } else {
        let mut seen = HashMap::new();

        for i in 1..1_000_000_000 {
            grid.advance_minute()?;

            let mut hasher = DefaultHasher::new();
            grid.cells.hash(&mut hasher);
            let hash_value = hasher.finish();

            match seen.entry(hash_value) {
                Entry::Occupied(entry) => {
                    let cycle_length = i - entry.get();
                    let remaining_hashes = (1_000_000_000 - i) % cycle_length;
                    for _ in 0..remaining_hashes {
                        grid.advance_minute()?;
                    }
                    return Ok(grid.resource_value());
                }
                Entry::Vacant(entry) => {
                    entry.insert(i);
                }
            }
        }
        Err("No solution found".to_string())
    }
}

#[test]
fn tests() {
    test_part_one!(
            ".#.#...|#.
.....#|##|
.|..|...#.
..|#.....#
#.#|||#|#|
...#.||...
.|....|...
||...#|.#|
|.||||..|.
...#.|..|."
        => 1147);

    let input = include_str!("day18_input.txt");
    test_part_one!(input => 531_417);
    test_part_two!(input => 205_296);
}