From 57eba7e0e559cc890e915e7d6f1db8adcf6dc98f Mon Sep 17 00:00:00 2001 From: Pat Thoyts Date: Sat, 7 Dec 2024 22:32:40 +0000 Subject: [PATCH] AoC 2024 days 1..7 --- .gitignore | 5 ++ day1/Cargo.toml | 7 +++ day1/src/main.rs | 104 ++++++++++++++++++++++++++++++ day2/Cargo.toml | 7 +++ day2/__init__.py | 65 +++++++++++++++++++ day2/__main__.py | 28 +++++++++ day2/src/main.rs | 52 +++++++++++++++ day2/src/reports.rs | 89 ++++++++++++++++++++++++++ day3/__init__.py | 89 ++++++++++++++++++++++++++ day3/__main__.py | 23 +++++++ day4/__init__.py | 95 ++++++++++++++++++++++++++++ day4/__main__.py | 24 +++++++ day5/__init__.py | 63 +++++++++++++++++++ day5/__main__.py | 26 ++++++++ day5/test_problem.py | 18 ++++++ day6/__init__.py | 133 +++++++++++++++++++++++++++++++++++++++ day6/__main__.py | 36 +++++++++++ day7/__init__.py | 45 +++++++++++++ day7/__main__.py | 24 +++++++ day_template/__init__.py | 15 +++++ day_template/__main__.py | 23 +++++++ 21 files changed, 971 insertions(+) create mode 100644 .gitignore create mode 100755 day1/Cargo.toml create mode 100755 day1/src/main.rs create mode 100644 day2/Cargo.toml create mode 100644 day2/__init__.py create mode 100644 day2/__main__.py create mode 100644 day2/src/main.rs create mode 100644 day2/src/reports.rs create mode 100755 day3/__init__.py create mode 100644 day3/__main__.py create mode 100755 day4/__init__.py create mode 100644 day4/__main__.py create mode 100644 day5/__init__.py create mode 100644 day5/__main__.py create mode 100644 day5/test_problem.py create mode 100644 day6/__init__.py create mode 100644 day6/__main__.py create mode 100644 day7/__init__.py create mode 100644 day7/__main__.py create mode 100644 day_template/__init__.py create mode 100644 day_template/__main__.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fec9490 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +Cargo.lock +target/ +data/ +__pycache__/ +.pytest_cache/ diff --git a/day1/Cargo.toml b/day1/Cargo.toml new file mode 100755 index 0000000..fdd2f19 --- /dev/null +++ b/day1/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "day1" +version = "0.1.0" +edition = "2021" + +[dependencies] +clap = { version = "4.5.22", features = ["derive"] } diff --git a/day1/src/main.rs b/day1/src/main.rs new file mode 100755 index 0000000..5e8ae7d --- /dev/null +++ b/day1/src/main.rs @@ -0,0 +1,104 @@ +extern crate clap; + +use std::error::Error; +use std::fs::File; +use std::io::{prelude::*, BufReader}; +use std::collections::BTreeMap; + +use clap::{Parser, ArgAction}; + +/// Advent of Code 2023 day 4 +#[derive(Parser, Default, Debug)] +struct Arguments { + /// specify the input data filename + filename: String, + #[arg(short, long, action=ArgAction::SetTrue)] + /// enable additional debug output + debug: Option, +} + +#[derive(Debug, PartialEq, Eq)] +pub struct ParseRowError; + +fn from_str(s: &str) -> Result<(i64, i64), ParseRowError> { + let (a, b) = s.trim().split_once(' ').ok_or(ParseRowError)?; + let l = a.trim().parse::().unwrap(); + let r = b.trim().parse::().unwrap(); + Ok((l, r)) +} + +fn main() -> Result<(), Box> { + let args = Arguments::parse(); + + let input = File::open(args.filename).expect("no such file"); + let buffered = BufReader::new(input); + + let (mut a, mut b): (Vec<_>,Vec<_>) = buffered.lines() + .map(|x| x.expect("invalid line")) + .map(|line| from_str(&line).unwrap()) + .into_iter() + .unzip(); + a.sort(); + b.sort(); + let cols = a.iter().zip(b).collect::>(); + let part1 = cols.iter().map(|(a, b)| (b - *a).abs()).sum::(); + println!("part 1: {}", part1); + + //let mut lset = BTreeMap::new(); + let mut rset = BTreeMap::new(); + for (_, r) in cols { + // lset.entry(l).and_modify(|v| *v += 1).or_insert(1_i64); + rset.entry(r).and_modify(|v| *v += 1).or_insert(1_i64); + } + + let mut part2 = 0_i64; + for k in a { + let count = *match rset.get(&k) { + Some(val) => val, + None => &0_i64 + }; + let s = k * count; + if args.debug.unwrap() { + println!("k:{} n:{} s:{}", k, count, s); + } + part2 += s; + } + println!("part 2: {}", part2); + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_lists_parse_rows() { + let input = "3 4\n4 3\n2 5\n1 3\n3 9\n3 3"; + + let (mut a, mut b): (Vec<_>,Vec<_>) = input.split('\n') + .map(|line| line.trim().split_once(' ').unwrap()) + .map(|(a, b)| (a.trim().parse::().unwrap(), b.trim().parse::().unwrap())) + .into_iter() + .unzip(); + + a.sort(); + assert_eq!(a, vec![1, 2, 3, 3, 3, 4]); + b.sort(); + assert_eq!(b, vec![3, 3, 3, 4, 5, 9]); + + let mut cols = a.into_iter().zip(b); + //assert_eq!(cols.next(), Some((1_i64, 3_i64))); + //assert_eq!(cols.next(), Some((2, 3))); + //assert_eq!(cols.next(), Some((3, 3))); + //assert_eq!(cols.next(), Some((3, 4))); + //assert_eq!(cols.next(), Some((3, 5))); + //assert_eq!(cols.next(), Some((4, 9))); + + let sum = cols.map(|(a, b)| (b - a).abs()).sum::(); + assert_eq!(sum, 11); + } + + //vec![1, 2, 3, 3, 3, 4], + //vec![3, 3, 3, 4, 5, 9], +} \ No newline at end of file diff --git a/day2/Cargo.toml b/day2/Cargo.toml new file mode 100644 index 0000000..7d48314 --- /dev/null +++ b/day2/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "day2" +version = "0.1.0" +edition = "2021" + +[dependencies] +clap = { version = "4.5.22", features = ["derive"] } diff --git a/day2/__init__.py b/day2/__init__.py new file mode 100644 index 0000000..d0deec4 --- /dev/null +++ b/day2/__init__.py @@ -0,0 +1,65 @@ +from typing import List +from dataclasses import dataclass + + +def is_safe(diff) -> bool: + rising = all([d > 0 for d in diff]) + falling = all([d < 0 for d in diff]) + small = all([abs(d) < 4 for d in diff]) + return (rising or falling) and small + + +@dataclass +class Record: + data: List[int] + diff: List[int] + + def is_safe(self) -> bool: + return is_safe(self.diff) + + def is_safe_dampened(self, debug=False) -> bool: + safe = False + for index, value in enumerate(self.data): + test = self.data.copy() + del test[index] # remove element by index + diff = Record.difference(test) + safe = is_safe(diff) + if debug: + print(f"value: {value} test:{test} diff:{diff} result:{safe}") + if safe: + break + return safe + + @staticmethod + def difference(data: List[int]) -> List[int]: + diff = [] + a = data[0] + for d in data[1:]: + diff.append(d - a) + a = d + return diff + + @staticmethod + def from_str(s: str) -> 'Record': + data = [int(v) for v in s.strip().split(' ')] + diff = Record.difference(data) + return Record(data, diff) + + +@dataclass +class Problem: + records: List[Record] + + def run(self, part2=False, debug=False) -> int: + safe = 0 + for record in self.records: + if record.is_safe(): + safe += 1 + elif part2 and record.is_safe_dampened(debug=debug): + safe += 1 + return safe + + @staticmethod + def from_stream(stream): + return Problem([Record.from_str(line) for line in stream]) + \ No newline at end of file diff --git a/day2/__main__.py b/day2/__main__.py new file mode 100644 index 0000000..1f52518 --- /dev/null +++ b/day2/__main__.py @@ -0,0 +1,28 @@ +import sys +import argparse +from . import Problem + + +def main(args=None): + parser = argparse.ArgumentParser(description="AOC 2024 day 2") + parser.add_argument('filename', type=str) + parser.add_argument('-d', '--debug', action='store_true') + parser.add_argument('-2', '--part2', action='store_true') + options = parser.parse_args(args) + + with open(options.filename) as f: + problem = Problem.from_stream(f) + # if options.debug: + # for record in problem.records: + # print(record.data, record.diff, record.is_safe()) + + if options.part2: + print(f"part2: {problem.run(part2=True, debug=options.debug)} safe reports.") + else: + print(f"part1: {problem.run()} safe reports.") + + return 0 + + +if __name__ == '__main__': + sys.exit(main(sys.argv[1:])) diff --git a/day2/src/main.rs b/day2/src/main.rs new file mode 100644 index 0000000..5e4db68 --- /dev/null +++ b/day2/src/main.rs @@ -0,0 +1,52 @@ +extern crate clap; + +pub mod reports; +use reports::Report; + +use std::error::Error; +use std::fs::File; +use std::io::{prelude::*, BufReader}; +use std::str::FromStr; + +use clap::{Parser, ArgAction}; + +/// Advent of Code 2024 day 2 +#[derive(Parser, Default, Debug)] +struct Arguments { + /// specify the input file name + filename: String, + #[arg(short, long, action=ArgAction::SetTrue)] + /// enable debug output + debug: Option, +} + + +fn parse_input(filename: &str) -> Vec { + let input = File::open(filename).expect("file not found"); + let buffered = BufReader::new(input); + buffered.lines() + .map(|line| Report::from_str(line.unwrap().as_str()).unwrap()) + .collect() +} + +fn part1(data: &Vec, debug: bool) -> i64 { + if debug { + for datum in data { + for d in datum.data.iter() { + print!("{} ", d); + } + println!(""); + } + } + 0_i64 +} + +fn main() -> Result<(), Box> { + let args = Arguments::parse(); + let debug = args.debug.unwrap_or(false); + let input = parse_input(&args.filename); + println!("part 1: {}", part1(&input, debug)); + //println!("part 2: {}", part2(&args.filename)); + + Ok(()) +} diff --git a/day2/src/reports.rs b/day2/src/reports.rs new file mode 100644 index 0000000..4c396e8 --- /dev/null +++ b/day2/src/reports.rs @@ -0,0 +1,89 @@ +use std::str::FromStr; + +#[derive(Debug, PartialEq, Eq)] +pub struct ParseReportError; + +pub type Level = i64; +// pub type Report = Vec; + +#[derive(Debug, PartialEq)] +pub struct Report { + pub data: Vec +} + +impl FromStr for Report { + type Err = ParseReportError; + + fn from_str(s: &str) -> Result { + let report = s + .trim() + .split_whitespace() + .map(|x| x.parse::().unwrap()) + .collect::>(); + Ok( Report {data: report }) + } +} + +impl Report { + pub fn is_safe(&self) -> bool { + + let mut i = self.data.iter(); + let a = i.next(); + let y = i.scan(a, |state, &x| { + let xx = x - *state; + *state = x; + Some(xx) + }); + + for x in y { + print!("{}, ", x); + } + + let deltas = self.data + .chunks_exact(2) + .map(|x| x.to_vec()) + .map(|x| x[1] - x[0]) + .collect::>(); + + print!("[ "); + for x in deltas.clone() { + print!("{} ", x); + } + + let allinc = deltas.clone().iter().all(|x| x > &0); + let alldec = deltas.clone().iter().all(|x| x < &0); + let allsmall = deltas.clone().iter().all(|x| x.abs() < 3); + println!("] inc {} dec {} small {}", allinc, alldec, allsmall); + (allinc || alldec) && allsmall + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_report_from_strln() { + let input = "7 6 4 2 1\n"; + let result = Report::from_str(input).unwrap(); + assert_eq!(result.data, vec![7_i64, 6, 4, 2, 1]); + } + #[test] + fn test_report_from_str() { + let input = "7 6 4 2 1"; + let result = Report::from_str(input).unwrap(); + assert_eq!(result.data, vec![7_i64, 6, 4, 2, 1]); + } + #[test] + fn test_report_is_safe_safe() { + let input = "7 6 4 2 1"; + let result = Report::from_str(input).unwrap(); + assert_eq!(result.is_safe(), true); + } + #[test] + fn test_report_is_safe_unsafe() { + let input = "1 2 7 8 9"; + let result = Report::from_str(input).unwrap(); + assert_eq!(result.is_safe(), false); + } +} diff --git a/day3/__init__.py b/day3/__init__.py new file mode 100755 index 0000000..ef1c9d0 --- /dev/null +++ b/day3/__init__.py @@ -0,0 +1,89 @@ +from enum import Enum +from dataclasses import dataclass + + +class State(Enum): + Junk = 0, + M = 1, + U = 2, + L = 3, + Open = 4, + Second = 5 + D = 6 + Oh = 7 + N = 8 + Apos = 9 + T = 10 + Open2 = 11 + + +@dataclass +class Problem: + data: list + + def run(self) -> int: + return sum([a * b for (a, b) in self.data]) + + @staticmethod + def from_stream(stream, part2=False): + data = [] + state = State.Junk + dostate = State.Junk + enabled = True + while True: + c = stream.read(1) + if not c: + break + if part2 and state == State.Junk: + if c == 'd' and dostate == State.Junk: + dostate = State.D + elif c == 'o' and dostate == State.D: + dostate = State.Oh + elif c == '(' and dostate == State.Oh: + dostate = State.Open + elif c == ')' and dostate == State.Open: + enabled = True + dostate = State.Junk + elif c == 'n' and dostate == State.Oh: + dostate = State.N + elif c == '\'' and dostate == State.N: + dostate = State.Apos + elif c == 't' and dostate == State.Apos: + dostate = State.T + elif c == '(' and dostate == State.T: + dostate = State.Open2 + elif c == ')' and dostate == State.Open2: + dostate = State.Junk + enabled = False + elif c == 'm': + state = State.M + elif state == State.Junk and c == 'm': + state = State.M + elif state == State.M and c == 'u': + state = State.U + elif state == State.U and c == 'l': + state = State.L + elif state == State.L and c == '(': + state = State.Open + pair = ["", ""] + elif state == State.Open: + if c == ',': + state = State.Second + elif c.isdigit() and len(pair[0]) < 3: + pair[0] += c + else: + state = State.Junk + elif state == State.Second: + if c == ')': + if enabled: + step = int(pair[0]), int(pair[1]) + data.append(step) + state = State.Junk + elif c.isdigit() and len(pair[1]) < 3: + pair[1] += c + else: + state = State.Junk + else: + state = State.Junk + + return Problem(data) diff --git a/day3/__main__.py b/day3/__main__.py new file mode 100644 index 0000000..ccf8c8e --- /dev/null +++ b/day3/__main__.py @@ -0,0 +1,23 @@ +import sys +import argparse +from . import Problem + + +def main(args=None): + parser = argparse.ArgumentParser(description="AOC 2024 day 3") + parser.add_argument('filename', type=str) + parser.add_argument('-d', '--debug', action='store_true') + parser.add_argument('-2', '--part2', action='store_true') + options = parser.parse_args(args) + + with open(options.filename) as f: + problem = Problem.from_stream(f, options.part2) + if options.debug: + for row in problem.data: + print(row) + print(f"result {problem.run()}") + return 0 + + +if __name__ == '__main__': + sys.exit(main(sys.argv[1:])) diff --git a/day4/__init__.py b/day4/__init__.py new file mode 100755 index 0000000..647feda --- /dev/null +++ b/day4/__init__.py @@ -0,0 +1,95 @@ +from enum import Enum +from dataclasses import dataclass +from typing import List + + +@dataclass +class Problem: + data: List[str] + word: str = 'XMAS' + + def run(self, part2: bool = False) -> int: + if part2: + return self.run_part2() + else: + return self.run_part1() + + def run_part1(self) -> int: + result = 0 + rows = len(self.data) + cols = len(self.data[0]) + for r in range(rows): + for c in range(cols): + result += self.wordcount(r, c) + return result + + def wordcount(self, row: int, col: int) -> int: + """if the current position is the start of our word """ + result = 0 + if self.data[row][col] == self.word[0]: + result += self.find_word(row, col, self.word[1]) + return result + + def find_word(self, row: int, col: int, want: str) -> int: + """check the Moore neighbourhood for the given letter and return + the number of words starting here.""" + count = 0 + for dr in [-1, 0, 1]: + r = row + dr + if r >= 0 and r < len(self.data): + for dc in [-1, 0, 1]: + c = col + dc + if c >= 0 and c < len(self.data[0]): + letter = self.data[r][c] + if letter == want: + dir = (dr, dc) + if self.verify((r, c), dir, 2): + count += 1 + return count + + def verify(self, pos: tuple, dir: tuple, start: int) -> bool: + """Given a position and direction vector check that we have a full + word returning True if so or False if not.""" + for n in range(start, len(self.word)): + newr = pos[0] + dir[0] + newc = pos[1] + dir[1] + if newr >= 0 and newr < len(self.data) and newc >= 0 and newc < len(self.data[0]): + pos = (newr, newc) + check = self.data[pos[0]][pos[1]] + if self.word[n] != check: + return False + else: + return False + return True + + def run_part2(self) -> int: + result = 0 + rows = len(self.data) + cols = len(self.data[0]) + for r in range(rows): + for c in range(cols): + if self.data[r][c] == 'A': + result += self.find_mas(r, c) + return result + + def find_mas(self, row: int, col: int) -> int: + count = 0 + for dr in [-1, 1]: + r = row + dr + r2 = row - dr + if r >= 0 and r < len(self.data) and r2 >= 0 and r2 < len(self.data): + for dc in [-1, 1]: + c = col + dc + c2 = col - dc + if c >= 0 and c < len(self.data[0]) and c2 >= 0 and c2 < len(self.data): + letter = self.data[r][c] + opposite = self.data[r2][c2] + if letter == 'M' and opposite == 'S': + # print((row, col), (r, c), (r2, c2), letter, 'A', opposite) + count += 1 + return 1 if count == 2 else 0 + + @staticmethod + def from_stream(stream) -> 'Problem': + data = [line.strip() for line in stream.readlines()] + return Problem(data) diff --git a/day4/__main__.py b/day4/__main__.py new file mode 100644 index 0000000..f34fb38 --- /dev/null +++ b/day4/__main__.py @@ -0,0 +1,24 @@ +import sys +import argparse +from . import Problem + + +def main(args=None): + parser = argparse.ArgumentParser(description="AOC 2024 day 4") + parser.add_argument('filename', type=str) + parser.add_argument('-d', '--debug', action='store_true') + parser.add_argument('-2', '--part2', action='store_true') + options = parser.parse_args(args) + + with open(options.filename) as f: + problem = Problem.from_stream(f) + if options.debug: + for row in problem.data: + print(row) + + print(f"result {problem.run(part2=options.part2)}") + return 0 + + +if __name__ == '__main__': + sys.exit(main(sys.argv[1:])) diff --git a/day5/__init__.py b/day5/__init__.py new file mode 100644 index 0000000..18b661a --- /dev/null +++ b/day5/__init__.py @@ -0,0 +1,63 @@ +from dataclasses import dataclass +from enum import Enum +from collections import defaultdict +from typing import List + + +class State(Enum): + Rules = 0, + Updates = 1 + + +@dataclass +class Problem: + rules: List[int] + updates: List[int] + preceding: defaultdict + + _disordered = [] + + def run(self) -> int: + result = sum([self.validate(ndx, pageset) \ + for ndx, pageset in enumerate(self.updates)]) + result2 = [self.revalidate(ndx) for ndx in self._disordered] + return result + + def validate(self, update_index: int, pageset: List[int]) -> int: + """If the pageset is valid, return the middle page number or 0 if invalid""" + # pageset = self.updates[update_index] + disallowed = set() + for page in pageset: + if page in disallowed: + self._disordered.append(update_index) + return 0 + disallowed |= self.preceding[page] + return pageset[len(pageset)//2] + + def revalidate(self, update_index: int) -> int: + pageset = self.updates[update_index] + # for page in pageset: + pass + + @staticmethod + def from_stream(stream) -> 'Problem': + state = State.Rules + rules = [] + updates = [] + for line in stream: + line = line.strip() + if len(line) == 0: + if state == State.Rules: + state = State.Updates + continue + if state == State.Rules: + rule = [int(a) for a in [x for x in line.split('|')]] + rules.append(rule) + elif state == State.Updates: + pageset = [int(p) for p in line.split(',')] + updates.append(pageset) + + preceding = defaultdict(set) + for a, b in rules: + preceding[b].add(a) + return Problem(rules, updates, preceding) diff --git a/day5/__main__.py b/day5/__main__.py new file mode 100644 index 0000000..e4b09b7 --- /dev/null +++ b/day5/__main__.py @@ -0,0 +1,26 @@ +import sys +import argparse +from . import Problem + + +def main(args=None): + parser = argparse.ArgumentParser(description="AOC 2024 day 5") + parser.add_argument('filename', type=str) + parser.add_argument('-d', '--debug', action='store_true') + parser.add_argument('-2', '--part2', action='store_true') + options = parser.parse_args(args) + + with open(options.filename) as f: + problem = Problem.from_stream(f) + if options.debug: + for rule in problem.rules: + print(rule) + for update in problem.updates: + print(update) + + print(f"result {problem.run()}") + return 0 + + +if __name__ == '__main__': + sys.exit(main(sys.argv[1:])) diff --git a/day5/test_problem.py b/day5/test_problem.py new file mode 100644 index 0000000..0366b7a --- /dev/null +++ b/day5/test_problem.py @@ -0,0 +1,18 @@ +import unittest +import os +from . import Problem + + +class TestProblem(unittest.TestCase): + testfile = os.path.join(os.path.dirname(__file__), r'data', r'test_input') + def test_from_str(self): + with open(self.testfile, 'rt') as fd: + problem = Problem.from_stream(fd) + self.assertEqual(len(problem.rules), 21) + self.assertSequenceEqual(problem.rules[0], [47, 53]) + self.assertEqual(len(problem.updates), 6) + self.assertSequenceEqual(problem.updates[0], [75, 47, 61, 53, 29]) + + +if __name__ == '__main__': + unittest.main() diff --git a/day6/__init__.py b/day6/__init__.py new file mode 100644 index 0000000..5d72098 --- /dev/null +++ b/day6/__init__.py @@ -0,0 +1,133 @@ +from dataclasses import dataclass +from typing import List, Tuple +from copy import copy, deepcopy +from enum import IntFlag + + +class Direction(IntFlag): + Left = 1 + Up = 2 + Right = 4 + Down = 8 + + +@dataclass +class Guard: + row: int + col: int + dir: str + + def move(self): + if self.dir == '<': + self.col -= 1 + elif self.dir == '^': + self.row -= 1 + elif self.dir == '>': + self.col += 1 + elif self.dir == 'v': + self.row += 1 + +DIRMAP = { + '<': Direction.Left, + '^': Direction.Up, + '>': Direction.Right, + 'v': Direction.Down +} + +@dataclass +class Problem: + data: List[int] + start: Guard + + def run(self, trace=False) -> int: + count = 1 + guard = copy(self.start) + self.data[guard.row][guard.col] = '0' + while True: + g = copy(guard) + g.move() + if self.outside(g): + break + next_tile = self.data[g.row][g.col] + if next_tile == '#': + if guard.dir == '^': + guard.dir = '>' + elif guard.dir == '>': + guard.dir = 'v' + elif guard.dir == 'v': + guard.dir = '<' + elif guard.dir == '<': + guard.dir = '^' + else: + if self.data[g.row][g.col] in ('.', '0'): + count += 1 + self.data[g.row][g.col] = '%c' % 0x60 + + # if the new tile has already been visited in the same + # direction then we are on a loop. + tileval = ord(self.data[g.row][g.col]) + if tileval & DIRMAP[g.dir]: + self.data[g.row][g.col] = 'O' + return -1 + + tileval |= DIRMAP[g.dir] + self.data[g.row][g.col] = '%c' % tileval # mark as visited + guard = g + + if self.outside(guard): + break + return count + + def next(self) -> str: + g = copy(self.guard) + g.move() + return self.data[g.row][g.col] + + def outside(self, guard: Guard) -> bool: + return ( + guard.row < 0 + or guard.col < 0 + or guard.row >= len(self.data) + or guard.col >= len(self.data[0]) + ) + + def unblocked(self): + for row, tiles in enumerate(self.data): + for col, tile in enumerate(tiles): + if tile == '.': + yield (row, col) + + def print_map(self, score) -> None: + print("".join(self.data[0]), score) + for row in self.data[1:]: + print("".join(row)) + print() + + def run_part2(self, debug=False) -> int: + count = 0 + old = deepcopy(self.data) + for coord in self.unblocked(): + self.data = deepcopy(old) + row, col = coord + self.data[row][col] = '#' + score = self.run(trace=debug) + if debug: + self.print_map(score) + if score < 0: + count += 1 + return count + + @staticmethod + def from_stream(stream) -> 'Problem': + data = [] + start = None + for row, line in enumerate(stream): + line = line.strip() + col = [n for n, c in enumerate(line) if c in ('<', '^', '>', 'v')] + if col: + start = (row, col[0]) + data.append([c for c in line]) + assert data + assert start is not None + guard = Guard(start[0], start[1], data[start[0]][start[1]]) + return Problem(data, guard) diff --git a/day6/__main__.py b/day6/__main__.py new file mode 100644 index 0000000..6e575df --- /dev/null +++ b/day6/__main__.py @@ -0,0 +1,36 @@ +import sys +import argparse +from . import Problem + + +def main(args=None): + parser = argparse.ArgumentParser(description="AOC 2024 day 6") + parser.add_argument('filename', type=str) + parser.add_argument('-d', '--debug', action='store_true') + parser.add_argument('-t', '--trace', action='store_true') + parser.add_argument('-1', '--part1', action='store_true') + parser.add_argument('-2', '--part2', action='store_true') + options = parser.parse_args(args) + + with open(options.filename) as f: + problem = Problem.from_stream(f) + if options.debug and options.part1: + print(problem.start) + for row in problem.data: + print("".join(row)) + + if options.part1: + print(f"result {problem.run(trace=options.trace)}") + if options.part2: + print(f"result {problem.run_part2(debug=options.debug)}") + + + if options.debug and options.part1: + for row in problem.data: + print("".join(row)) + + return 0 + + +if __name__ == '__main__': + sys.exit(main(sys.argv[1:])) diff --git a/day7/__init__.py b/day7/__init__.py new file mode 100644 index 0000000..43e39f0 --- /dev/null +++ b/day7/__init__.py @@ -0,0 +1,45 @@ +from dataclasses import dataclass +from typing import List, Tuple + + +__title__ = 'Day 7: Bridge Repair' + + +@dataclass +class Problem: + data: List[Tuple[int, List[int]]] + + def run(self, part2=False) -> int: + total = 0 + for result, nums in self.data: + total += Problem.validate(result, nums, part2) + return total + + @staticmethod + def validate(result: int, nums: List[int], with_concat=False) -> int: + queue = [(1, nums[0])] + while queue: + index, value = queue.pop() + if index == len(nums): + if value == result: + return result + continue + calcs = [value + nums[index], + value * nums[index]] + if with_concat: + calcs.append( + int(str(value) + str(nums[index]))) + for calc in calcs: + if calc <= result: + queue.append( (index + 1, calc)) + return 0 + + @staticmethod + def from_stream(stream) -> 'Problem': + data = [] + for line in stream: + result, nums = line.strip().split(':') + result = int(result) + nums = [int(n) for n in nums.strip().split(' ')] + data.append((result, tuple(nums))) + return Problem(data) diff --git a/day7/__main__.py b/day7/__main__.py new file mode 100644 index 0000000..7f7504d --- /dev/null +++ b/day7/__main__.py @@ -0,0 +1,24 @@ +import sys +import argparse +from . import Problem + + +def main(args=None): + parser = argparse.ArgumentParser(description="AOC 2024 day 7") + parser.add_argument('filename', type=str) + parser.add_argument('-d', '--debug', action='store_true') + parser.add_argument('-2', '--part2', action='store_true') + options = parser.parse_args(args) + + with open(options.filename) as f: + problem = Problem.from_stream(f) + if options.debug: + for row in problem.data: + print(row) + + print(f"result {problem.run(part2=options.part2)}") + return 0 + + +if __name__ == '__main__': + sys.exit(main(sys.argv[1:])) diff --git a/day_template/__init__.py b/day_template/__init__.py new file mode 100644 index 0000000..eba9d6e --- /dev/null +++ b/day_template/__init__.py @@ -0,0 +1,15 @@ +from dataclasses import dataclass +from typing import List + + +@dataclass +class Problem: + data: List[int] + + def run(self) -> int: + return 0 + + @staticmethod + def from_stream(stream) -> 'Problem': + data = [line.strip() for line in stream] + return Problem(data) diff --git a/day_template/__main__.py b/day_template/__main__.py new file mode 100644 index 0000000..eb12339 --- /dev/null +++ b/day_template/__main__.py @@ -0,0 +1,23 @@ +import sys +import argparse +from . import Problem + + +def main(args=None): + parser = argparse.ArgumentParser(description="AOC 2024 day ?") + parser.add_argument('filename', type=str) + parser.add_argument('-d', '--debug', action='store_true') + options = parser.parse_args(args) + + with open(options.filename) as f: + problem = Problem.from_stream(f) + if options.debug: + for row in problem.data: + print(row) + + print(f"result {problem.run()}") + return 0 + + +if __name__ == '__main__': + sys.exit(main(sys.argv[1:])) -- 2.23.0