aoc/year2015/
day22.rs

1//! # Wizard Simulator 20XX
2//!
3//! [Dijkstra's algorithm](https://en.wikipedia.org/wiki/Dijkstra%27s_algorithm) is ideal
4//! for solving this problem. A node in the graph is our current state and each edge is
5//! represented by casting a spell to get to a new state.
6//!
7//! The key to optimizing is to cache previously seen states. As we receive states in strictly
8//! increasing order of mana spent if we see a state again then it cannot possibly be optimal
9//! and we can discard.
10use crate::util::hash::*;
11use crate::util::heap::*;
12use crate::util::iter::*;
13use crate::util::parse::*;
14
15type Input = [i16; 2];
16
17#[derive(Clone, Copy, PartialEq, Eq, Hash)]
18struct State {
19    boss_hp: i16,
20    player_hp: i16,
21    player_mana: i16,
22    shield_effect: u8,
23    poison_effect: u8,
24    recharge_effect: u8,
25}
26
27impl State {
28    /// Applies spell effects to state and returns true if the player has won.
29    #[inline]
30    fn apply_spell_effects(&mut self) -> bool {
31        if self.shield_effect > 0 {
32            self.shield_effect -= 1;
33        }
34        if self.poison_effect > 0 {
35            self.poison_effect -= 1;
36            self.boss_hp -= 3;
37        }
38        if self.recharge_effect > 0 {
39            self.recharge_effect -= 1;
40            self.player_mana += 101;
41        }
42
43        self.boss_hp <= 0
44    }
45
46    /// Applies boss attack and returns true if the wizard survives.
47    #[inline]
48    fn boss_turn(&mut self, mut attack: i16) -> bool {
49        if self.shield_effect > 0 {
50            attack = (attack - 7).max(1);
51        }
52
53        self.player_hp -= attack;
54        self.player_hp > 0 && self.player_mana >= 53
55    }
56}
57
58pub fn parse(input: &str) -> Input {
59    input.iter_signed().chunk::<2>().next().unwrap()
60}
61
62pub fn part1(input: &Input) -> i16 {
63    play(*input, false)
64}
65
66pub fn part2(input: &Input) -> i16 {
67    play(*input, true)
68}
69
70fn play(input: Input, hard_mode: bool) -> i16 {
71    let [boss_hp, boss_damage] = input;
72    let start = State {
73        boss_hp,
74        player_hp: 50,
75        player_mana: 500,
76        shield_effect: 0,
77        poison_effect: 0,
78        recharge_effect: 0,
79    };
80
81    let mut todo = MinHeap::new();
82    let mut cache = FastSet::with_capacity(5_000);
83
84    todo.push(0, start);
85    cache.insert(start);
86
87    while let Some((spent, mut state)) = todo.pop() {
88        // Check winning condition
89        if state.apply_spell_effects() {
90            return spent;
91        }
92
93        // Part two
94        if hard_mode {
95            if state.player_hp > 1 {
96                state.player_hp -= 1;
97            } else {
98                continue;
99            }
100        }
101
102        // Magic Missile
103        if state.player_mana >= 53 {
104            let mut next =
105                State { boss_hp: state.boss_hp - 4, player_mana: state.player_mana - 53, ..state };
106
107            if next.apply_spell_effects() {
108                return spent + 53;
109            }
110            if next.boss_turn(boss_damage) && cache.insert(next) {
111                todo.push(spent + 53, next);
112            }
113        }
114
115        // Drain
116        if state.player_mana >= 73 {
117            let mut next = State {
118                boss_hp: state.boss_hp - 2,
119                player_hp: state.player_hp + 2,
120                player_mana: state.player_mana - 73,
121                ..state
122            };
123
124            if next.apply_spell_effects() {
125                return spent + 73;
126            }
127            if next.boss_turn(boss_damage) && cache.insert(next) {
128                todo.push(spent + 73, next);
129            }
130        }
131
132        // Shield
133        if state.player_mana >= 113 && state.shield_effect == 0 {
134            let mut next =
135                State { player_mana: state.player_mana - 113, shield_effect: 6, ..state };
136
137            if next.apply_spell_effects() {
138                return spent + 113;
139            }
140            if next.boss_turn(boss_damage) && cache.insert(next) {
141                todo.push(spent + 113, next);
142            }
143        }
144
145        // Poison
146        if state.player_mana >= 173 && state.poison_effect == 0 {
147            let mut next =
148                State { player_mana: state.player_mana - 173, poison_effect: 6, ..state };
149
150            if next.apply_spell_effects() {
151                return spent + 173;
152            }
153            if next.boss_turn(boss_damage) && cache.insert(next) {
154                todo.push(spent + 173, next);
155            }
156        }
157
158        // Recharge
159        if state.player_mana >= 229 && state.recharge_effect == 0 {
160            let mut next =
161                State { player_mana: state.player_mana - 229, recharge_effect: 5, ..state };
162
163            if next.apply_spell_effects() {
164                return spent + 229;
165            }
166            if next.boss_turn(boss_damage) && cache.insert(next) {
167                todo.push(spent + 229, next);
168            }
169        }
170    }
171
172    unreachable!()
173}