|
| 1 | +//! [Day 21: RPG Simulator 20XX](https://adventofcode.com/2015/day/21) |
| 2 | +
|
| 3 | +//! [Day 21: RPG Simulator 20XX](https://adventofcode.com/2015/day/21) |
| 4 | +use std::cmp::{max, min}; |
| 5 | +use std::str::FromStr; |
| 6 | + |
| 7 | +#[derive(Debug, Clone)] |
| 8 | +struct Character { |
| 9 | + _name: String, |
| 10 | + hitpoints: i32, |
| 11 | + damage: i32, |
| 12 | + armor: i32, |
| 13 | +} |
| 14 | + |
| 15 | +impl Character { |
| 16 | + fn new(name: &str, hitpoints: i32, damage: i32, armor: i32) -> Self { |
| 17 | + Self { |
| 18 | + _name: name.to_string(), |
| 19 | + hitpoints, |
| 20 | + damage, |
| 21 | + armor, |
| 22 | + } |
| 23 | + } |
| 24 | + |
| 25 | + fn attack(&self, enemy: &mut Self) { |
| 26 | + let damage = max(0, self.damage - enemy.armor); |
| 27 | + enemy.hitpoints -= damage; |
| 28 | + |
| 29 | + if enemy.hitpoints < 0 { |
| 30 | + enemy.hitpoints = 0; |
| 31 | + } |
| 32 | + |
| 33 | + // Optional: Uncomment for debugging |
| 34 | + // println!( |
| 35 | + // "The {} deals {}-{} = {} damage; the {} goes down to {} hit points.", |
| 36 | + // self.name, self.damage, enemy.armor, damage, enemy.name, enemy.hitpoints |
| 37 | + // ); |
| 38 | + } |
| 39 | +} |
| 40 | + |
| 41 | +fn combat(c1: &mut Character, c2: &mut Character) -> i32 { |
| 42 | + loop { |
| 43 | + c1.attack(c2); |
| 44 | + if c2.hitpoints == 0 { |
| 45 | + return 1; |
| 46 | + } |
| 47 | + |
| 48 | + c2.attack(c1); |
| 49 | + if c1.hitpoints == 0 { |
| 50 | + return 2; |
| 51 | + } |
| 52 | + } |
| 53 | +} |
| 54 | + |
| 55 | +#[derive(Debug, Clone, Copy, PartialEq, Eq)] |
| 56 | +struct Item { |
| 57 | + cost: i32, |
| 58 | + damage: i32, |
| 59 | + armor: i32, |
| 60 | +} |
| 61 | + |
| 62 | +fn parse_items(input: &str) -> Vec<Item> { |
| 63 | + input |
| 64 | + .lines() |
| 65 | + .filter(|line| !line.trim().is_empty()) |
| 66 | + .map(|line| { |
| 67 | + let parts: Vec<&str> = line.split_whitespace().collect(); |
| 68 | + let cost = i32::from_str(parts[parts.len() - 3]).unwrap(); |
| 69 | + let damage = i32::from_str(parts[parts.len() - 2]).unwrap(); |
| 70 | + let armor = i32::from_str(parts[parts.len() - 1]).unwrap(); |
| 71 | + Item { |
| 72 | + cost, |
| 73 | + damage, |
| 74 | + armor, |
| 75 | + } |
| 76 | + }) |
| 77 | + .collect() |
| 78 | +} |
| 79 | + |
| 80 | +fn play(boss: &Character) -> (i32, i32) { |
| 81 | + let weapons = parse_items( |
| 82 | + r" |
| 83 | +Dagger 8 4 0 |
| 84 | +Shortsword 10 5 0 |
| 85 | +Warhammer 25 6 0 |
| 86 | +Longsword 40 7 0 |
| 87 | +Greataxe 74 8 0 |
| 88 | +", |
| 89 | + ); |
| 90 | + |
| 91 | + let mut armors = parse_items( |
| 92 | + r" |
| 93 | +Leather 13 0 1 |
| 94 | +Chainmail 31 0 2 |
| 95 | +Splintmail 53 0 3 |
| 96 | +Bandedmail 75 0 4 |
| 97 | +Platemail 102 0 5 |
| 98 | +", |
| 99 | + ); |
| 100 | + |
| 101 | + let mut rings = parse_items( |
| 102 | + r" |
| 103 | +Damage +1 25 1 0 |
| 104 | +Damage +2 50 2 0 |
| 105 | +Damage +3 100 3 0 |
| 106 | +Defense +1 20 0 1 |
| 107 | +Defense +2 40 0 2 |
| 108 | +Defense +3 80 0 3 |
| 109 | +", |
| 110 | + ); |
| 111 | + |
| 112 | + // Add "no armor" and "no ring" options |
| 113 | + armors.push(Item { |
| 114 | + cost: 0, |
| 115 | + damage: 0, |
| 116 | + armor: 0, |
| 117 | + }); |
| 118 | + rings.push(Item { |
| 119 | + cost: 0, |
| 120 | + damage: 0, |
| 121 | + armor: 0, |
| 122 | + }); |
| 123 | + |
| 124 | + let mut min_win_cost = i32::MAX; |
| 125 | + let mut max_loose_cost = 0; |
| 126 | + |
| 127 | + for w in &weapons { |
| 128 | + for a in &armors { |
| 129 | + for r1 in &rings { |
| 130 | + for r2 in &rings { |
| 131 | + // Cannot buy two of the same rings |
| 132 | + if r1 == r2 { |
| 133 | + continue; |
| 134 | + } |
| 135 | + |
| 136 | + let cost = w.cost + a.cost + r1.cost + r2.cost; |
| 137 | + let damage = w.damage + a.damage + r1.damage + r2.damage; |
| 138 | + let armor = w.armor + a.armor + r1.armor + r2.armor; |
| 139 | + |
| 140 | + let mut player = Character::new("player", 100, damage, armor); |
| 141 | + let mut boss_copy = boss.clone(); |
| 142 | + |
| 143 | + if combat(&mut player, &mut boss_copy) == 1 { |
| 144 | + min_win_cost = min(min_win_cost, cost); |
| 145 | + } else { |
| 146 | + max_loose_cost = max(max_loose_cost, cost); |
| 147 | + } |
| 148 | + } |
| 149 | + } |
| 150 | + } |
| 151 | + } |
| 152 | + |
| 153 | + (min_win_cost, max_loose_cost) |
| 154 | +} |
| 155 | + |
| 156 | +struct Puzzle { |
| 157 | + min_win_cost: i32, |
| 158 | + max_loose_cost: i32, |
| 159 | +} |
| 160 | + |
| 161 | +impl Puzzle { |
| 162 | + const fn new() -> Self { |
| 163 | + Self { |
| 164 | + min_win_cost: 0, |
| 165 | + max_loose_cost: 0, |
| 166 | + } |
| 167 | + } |
| 168 | + |
| 169 | + /// Get the puzzle input. |
| 170 | + fn solve(&mut self, path: &str) { |
| 171 | + let data = std::fs::read_to_string(path).unwrap_or_else(|_| { |
| 172 | + eprintln!("cannot read input file {path}"); |
| 173 | + std::process::exit(1); |
| 174 | + }); |
| 175 | + |
| 176 | + let mut boss = Character::new("boss", 0, 0, 0); |
| 177 | + |
| 178 | + for line in data.lines() { |
| 179 | + let parts: Vec<&str> = line.split(": ").collect(); |
| 180 | + match parts.first() { |
| 181 | + Some(&"Hit Points") => boss.hitpoints = parts[1].parse().unwrap(), |
| 182 | + Some(&"Damage") => boss.damage = parts[1].parse().unwrap(), |
| 183 | + Some(&"Armor") => boss.armor = parts[1].parse().unwrap(), |
| 184 | + _ => {} |
| 185 | + } |
| 186 | + } |
| 187 | + |
| 188 | + (self.min_win_cost, self.max_loose_cost) = play(&boss); |
| 189 | + } |
| 190 | +} |
| 191 | + |
| 192 | +fn main() { |
| 193 | + let args = aoc::parse_args(); |
| 194 | + let mut puzzle = Puzzle::new(); |
| 195 | + puzzle.solve(args.path.as_str()); |
| 196 | + println!("{}", puzzle.min_win_cost); |
| 197 | + println!("{}", puzzle.max_loose_cost); |
| 198 | +} |
0 commit comments