From 79c8fe048a7043b77b8ca4ce2be0f6d4f736bb48 Mon Sep 17 00:00:00 2001 From: FaultyBranches Date: Fri, 5 Dec 2025 17:06:59 -0600 Subject: [PATCH] feat: Actually day 4 of '25 --- 25/3/main.py | 3 +- 25/4/main.py | 78 +++++++++++++++++++++++++++++++++++++++++++++++ 25/4/test_main.py | 40 ++++++++++++++++++++++++ 3 files changed, 119 insertions(+), 2 deletions(-) create mode 100644 25/4/main.py create mode 100644 25/4/test_main.py diff --git a/25/3/main.py b/25/3/main.py index 6f2a777..bb0a66c 100644 --- a/25/3/main.py +++ b/25/3/main.py @@ -26,9 +26,8 @@ def get_highest_dynamic(bank_size: int, input: str) -> int: for i in range(bank_size, 0, -1): curr_highest = 0 highest_index = 0 - limit = len(input) - i + 1 - for j in range(curr_index, limit): + for j in range(curr_index, len(input) - i + 1): found = int(input[j]) if found > curr_highest: diff --git a/25/4/main.py b/25/4/main.py new file mode 100644 index 0000000..010ca52 --- /dev/null +++ b/25/4/main.py @@ -0,0 +1,78 @@ +def num_neighbours(lines: [], line_index: int, pos_index: int, num_lines: int, line_length: int) -> int: + '''Check surrounding and current row neighbours of a given position. + Does not operate on 2d coords directly, but on index and current versus prev/next lines. + ''' + total = 0 + + for ii in range(max(0, line_index - 1), min(num_lines, line_index + 2)): + for jj in range(max(0, pos_index - 1), min(line_length, pos_index + 2)): + if ii == line_index and jj == pos_index: + continue + + if lines[ii][jj] == '@': + total += 1 + + return total + +def process_line(lines: []) -> int: + '''Iterate through all lines in the map and determine which can be removed this time around''' + total = 0 + + for line_index, line in enumerate(lines): + for index, char in enumerate(line): + if char == '@': + total_neighbours = num_neighbours(lines, line_index, index, len(lines), len(line)) + + if total_neighbours < 4: + total += 1 + + return total + + +def find_all_accessible_rolls(lines: []) -> int: + '''Recursively iterate through removing paper rolls and see how many are opened up for removal after each pass until none can be removed''' + total = 0 + + # Copy the map to note which have been removed without affecting the current checks + lines_copy = lines + + for line_index, line in enumerate(lines): + for index, char in enumerate(line): + if char == '@': + total_neighbours = 0 + + for ii in range(max(0, line_index - 1), min(len(lines), line_index + 2)): + for jj in range(max(0, index - 1), min(len(line), index + 2)): + if ii == line_index and jj == index: + continue + + if lines[ii][jj] == '@': + total_neighbours += 1 + + if total_neighbours < 4: + total += 1 + temp_line = list(lines_copy[line_index]) + temp_line[index] = '.' + lines_copy[line_index] = ''.join(temp_line) + + if total != 0: + return total + find_all_accessible_rolls(lines_copy) + + return total + + +def main(): + '''Entrypoint''' + with open('input.txt', encoding='utf-8') as fh: + all_lines = fh.readlines() + part1_total = process_line(all_lines) + + print(f'Part 1: {part1_total}') + + part2_total = find_all_accessible_rolls(all_lines) + + print(f'Part 2: {part2_total}') + + +if __name__ == "__main__": + main() diff --git a/25/4/test_main.py b/25/4/test_main.py new file mode 100644 index 0000000..c44c7af --- /dev/null +++ b/25/4/test_main.py @@ -0,0 +1,40 @@ +import pytest + +from main import process_line, find_all_accessible_rolls + +@pytest.mark.parametrize('lines, expected', + [ + ([ + '..@@.@@@@.', + '@@@.@.@.@@', + '@@@@@.@.@@', + '@.@@@@..@.', + '@@.@@@@.@@', + '.@@@@@@@.@', + '.@.@.@.@@@', + '@.@@@.@@@@', + '.@@@@@@@@.', + '@.@.@@@.@.' + ], 13), + ]) +def test_process_line(lines, expected): + assert process_line(lines) == expected + + +@pytest.mark.parametrize('lines, expected', + [ + ([ + '..@@.@@@@.', + '@@@.@.@.@@', + '@@@@@.@.@@', + '@.@@@@..@.', + '@@.@@@@.@@', + '.@@@@@@@.@', + '.@.@.@.@@@', + '@.@@@.@@@@', + '.@@@@@@@@.', + '@.@.@@@.@.' + ], 43), + ]) +def test_iterative_processing(lines, expected): + assert find_all_accessible_rolls(lines) == expected