--- /dev/null
+use regex::Regex;
+use std::ops::{Add, Sub};
+
+#[derive(Debug, Hash, Eq, PartialEq, Copy, Clone)]
+struct Point(i32, i32);
+
+#[rustfmt::skip]
+const DIRS: [Point; 4] = [
+ Point(-1, 0),
+ Point(0, -1), /* ctr */ Point(0, 1),
+ Point( 1, 0),
+];
+
+impl Point {
+ fn in_bounds(&self, bound: usize) -> bool {
+ let bound: i32 = bound.try_into().unwrap();
+ self.0 >= 0 && self.1 >= 0 && self.0 < bound && self.1 < bound
+ }
+
+ fn adjacent(self, bound: usize) -> impl Iterator<Item = Point> {
+ self.adjacent_unchecked()
+ .filter(move |p| p.in_bounds(bound))
+ }
+
+ fn adjacent_unchecked(self) -> impl Iterator<Item = Point> {
+ DIRS.iter().copied().map(move |d| self + d)
+ }
+
+ fn scale(&self, multiplier: i32) -> Point {
+ Point(self.0 * multiplier, self.1 * multiplier)
+ }
+
+ fn cost(&self) -> u64 {
+ (self.0 * 3 + self.1).try_into().unwrap()
+ }
+}
+
+impl Add for Point {
+ type Output = Point;
+
+ fn add(self, rhs: Self) -> Self::Output {
+ Point(self.0 + rhs.0, self.1 + rhs.1)
+ }
+}
+
+impl Sub for Point {
+ type Output = Point;
+
+ fn sub(self, rhs: Self) -> Self::Output {
+ Point(self.0 - rhs.0, self.1 - rhs.1)
+ }
+}
+
+#[derive(Debug, Hash, Eq, PartialEq, Copy, Clone)]
+struct Game(Point, Point, Point);
+
+fn parse(input: &str) -> Vec<Game> {
+ Regex::new(
+ r"Button A: X\+(\d+), Y\+(\d+)\sButton B: X\+(\d+), Y\+(\d+)\sPrize: X=(\d+), Y=(\d+)\s",
+ )
+ .unwrap()
+ .captures_iter(input)
+ .map(|c| c.extract())
+ .map(|(_, [ax, ay, bx, by, px, py])| {
+ Game(
+ Point(ax.parse().unwrap(), ay.parse().unwrap()),
+ Point(bx.parse().unwrap(), by.parse().unwrap()),
+ Point(px.parse().unwrap(), py.parse().unwrap()),
+ )
+ })
+ .collect()
+}
+
+#[derive()]
+struct ButtonSeq {
+ curr: Option<Point>,
+}
+
+impl ButtonSeq {
+ fn new() -> Self {
+ ButtonSeq { curr: None }
+ }
+}
+
+impl Iterator for ButtonSeq {
+ type Item = Point;
+
+ fn next(&mut self) -> Option<Self::Item> {
+ self.curr = match self.curr {
+ None => Some(Point(0, 1)),
+ Some(Point(a, b)) if b < 3 => Some(Point(0, 3 * a + b + 1)),
+ Some(Point(a, b)) => Some(Point(a + 1, b - 3)),
+ };
+ self.curr
+ }
+}
+
+impl Game {
+ fn check_win(&self, seq: Point) -> bool {
+ self.0.scale(seq.0) + self.1.scale(seq.1) == self.2
+ }
+
+ fn find_min_winning(&self) -> Option<Point> {
+ ButtonSeq::new().find(|&seq| self.check_win(seq))
+ }
+
+ fn find_min_winning_dumb(&self) -> Option<Point> {
+ let mut opts: Vec<_> = (0..=100)
+ .flat_map(move |i| (0..=100).map(move |j| Point(i, j)))
+ .skip(1)
+ .filter(|&p| self.check_win(p))
+ .collect();
+ opts.sort_by_key(|p| p.cost());
+ opts.first().copied()
+ }
+}
+
+pub fn part1(input: &str) -> u64 {
+ parse(input)
+ .iter()
+ .map(|g| g.find_min_winning_dumb())
+ .map(|s| s.map(|s| s.cost()).unwrap_or_default())
+ .sum()
+}
+
+pub fn part2(input: &str) -> u64 {
+ parse(input)
+ .iter()
+ .map(|g| g.find_min_winning())
+ .map(|s| s.map(|s| s.cost()).unwrap_or_default())
+ .sum()
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ const INPUT_STR: &str = concat!(
+ "Button A: X+94, Y+34\n",
+ "Button B: X+22, Y+67\n",
+ "Prize: X=8400, Y=5400\n",
+ "\n",
+ "Button A: X+26, Y+66\n",
+ "Button B: X+67, Y+21\n",
+ "Prize: X=12748, Y=12176\n",
+ "\n",
+ "Button A: X+17, Y+86\n",
+ "Button B: X+84, Y+37\n",
+ "Prize: X=7870, Y=6450\n",
+ "\n",
+ "Button A: X+69, Y+23\n",
+ "Button B: X+27, Y+71\n",
+ "Prize: X=18641, Y=10279\n",
+ );
+
+ #[test]
+ fn test_parse() {
+ assert_eq!(
+ parse(INPUT_STR),
+ [
+ Game(Point(94, 34), Point(22, 67), Point(8400, 5400)),
+ Game(Point(26, 66), Point(67, 21), Point(12748, 12176)),
+ Game(Point(17, 86), Point(84, 37), Point(7870, 6450)),
+ Game(Point(69, 23), Point(27, 71), Point(18641, 10279))
+ ]
+ )
+ }
+
+ #[rustfmt::skip]
+ #[test]
+ fn test_buttonseq() {
+ assert_eq!(
+ ButtonSeq::new().take(21).collect::<Vec<_>>(),
+ [
+ // Point(0, 0), -- excluded
+ Point(0, 1),
+ Point(0, 2),
+ Point(0, 3), Point(1, 0),
+ Point(0, 4), Point(1, 1),
+ Point(0, 5), Point(1, 2),
+ Point(0, 6), Point(1, 3), Point(2, 0),
+ Point(0, 7), Point(1, 4), Point(2, 1),
+ Point(0, 8), Point(1, 5), Point(2, 2),
+ Point(0, 9), Point(1, 6), Point(2, 3), Point(3, 0),
+ ]
+ );
+
+ let table = [
+ (Point(32, 3), Point(33, 0)),
+ (Point(33, 0), Point(0, 100)),
+ (Point(33, 1), Point(1, 98)),
+ (Point(33, 2), Point(1, 99)),
+ (Point(34, 0), Point(1, 100)),
+ (Point(34, 1), Point(2, 98)),
+ (Point(34, 2), Point(2, 99)),
+ (Point(35, 0), Point(2, 100)),
+ // ... skip a bit ...
+ // (Point(98, 0), Point(66, 100)),
+ // (Point(99, 0), Point(66, 100)),
+ ];
+ for (curr, next) in table {
+ assert_eq!(ButtonSeq { curr: Some(curr) }.next(), Some(next));
+ }
+ }
+
+ #[test]
+ fn test_find_min_winning() {
+ assert_eq!(
+ Game(Point(94, 34), Point(22, 67), Point(8400, 5400)).find_min_winning(),
+ Some(Point(80, 40))
+ );
+ assert_eq!(
+ Game(Point(26, 66), Point(67, 21), Point(12748, 12176)).find_min_winning(),
+ None
+ );
+ assert_eq!(
+ Game(Point(17, 86), Point(84, 37), Point(7870, 6450)).find_min_winning(),
+ Some(Point(38, 86))
+ );
+ assert_eq!(
+ Game(Point(69, 23), Point(27, 71), Point(18641, 10279)).find_min_winning(),
+ None
+ );
+ }
+
+ #[test]
+ fn test_winnings() {
+ assert_eq!(part1(INPUT_STR), 480)
+ }
+
+ #[test]
+ fn test_part1() {
+ assert_eq!(part1(&crate::input(13).unwrap()), 36954)
+ }
+
+ #[test]
+ #[ignore]
+ fn test_part2() {
+ assert_eq!(part2(&crate::input(13).unwrap()), 0)
+ }
+}