diff --git a/CHANGELOG.md b/CHANGELOG.md
index ab05992..1d23f12 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,10 @@ Of note:
- The changelog 2015.5.2 has been rewritten from each commit content.
- This file may be amended entirely in the future to adhere to the [GNU Changelog style](https://www.gnu.org/prep/standards/html_node/Style-of-Change-Logs.html#Style-of-Change-Logs)
+## [2015.21.1]
+### Added
+- Solved [exercice for 2015, day 21](src/year_2015/day_21.rs).
+
## [2015.20.2]
### Added
- More documentation.
diff --git a/Cargo.toml b/Cargo.toml
index 1af7517..594c950 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "advent-rs"
-version = "2015.20.2"
+version = "2015.21.1"
edition = "2021"
authors = ["Arnaud 'red' Rouyer"]
readme = "README.md"
diff --git a/NOTES_2015.md b/NOTES_2015.md
index 8aeda80..72a5be7 100644
--- a/NOTES_2015.md
+++ b/NOTES_2015.md
@@ -586,3 +586,27 @@ year_2015::day_20/year_2015::day_20_v2
I remembered this one was A PIECE OF SHIT to run in Ruby, as it lasted around over minute on my machine the last time I ran it. Having it run in only one second here is a real pleasure. Some issues flip-flapping between types, but nothing too brutal.
+
+## Day 21: RPG Simulator 20XX
+
+
+📊Tests and benchmarks
+
+```
+test year_2015::day_20::tests::works_with_samples_v1 ... ok
+test year_2015::tests::day_20 ... ok
+
+year_2015::day_21/year_2015::day_21_v1
+ time: [4.8485 µs 4.8549 µs 4.8618 µs]
+year_2015::day_21/year_2015::day_21_v2
+ time: [4.8473 µs 4.8539 µs 4.8609 µs]
+```
+
+
+
+Ruby version comments
+
+> ...really nothing to say here.
+
+
+Couldn't believe this one is running so fast.
diff --git a/README.md b/README.md
index c30d897..3e5d2bd 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
As I said, [Consistency is hard](https://github.com/joshleaves/advent-rb), and I haven't taken the time to pick up a new language in quite a while.
I'm also adding notes that may be useful if you're (like me) discovering Rust:
-- [2015, up to day 20](NOTES_2015.md)
+- [2015, up to day 21](NOTES_2015.md)
# Regarding style rules
I'm gonna use a mix of what `cargo fmt` does, with some stuff that feels more natural to me.
diff --git a/benches/advent-bench.rs b/benches/advent-bench.rs
index b5fde12..36e30b5 100644
--- a/benches/advent-bench.rs
+++ b/benches/advent-bench.rs
@@ -18,6 +18,7 @@ use advent_rs::year_2015::day_17;
use advent_rs::year_2015::day_18;
use advent_rs::year_2015::day_19;
use advent_rs::year_2015::day_20;
+use advent_rs::year_2015::day_21;
use criterion::{black_box, criterion_group, criterion_main, Criterion};
pub fn year_2015_benchmark(c: &mut Criterion) {
@@ -222,6 +223,16 @@ pub fn year_2015_benchmark(c: &mut Criterion) {
b.iter(|| day_20::day_20_v2(black_box(input_year_2015_day_20)))
});
g2015_day_20.finish();
+
+ let mut g2015_day_21 = c.benchmark_group("year_2015::day_21");
+ let input_year_2015_day_21 = include_str!("../inputs/year_2015_day_21_input");
+ g2015_day_21.bench_function("year_2015::day_21_v1", |b| {
+ b.iter(|| day_21::day_21_v1(black_box(input_year_2015_day_21)))
+ });
+ g2015_day_21.bench_function("year_2015::day_21_v2", |b| {
+ b.iter(|| day_21::day_21_v2(black_box(input_year_2015_day_21)))
+ });
+ g2015_day_21.finish();
}
criterion_group!(benches, year_2015_benchmark);
diff --git a/inputs/year_2015_day_21_input b/inputs/year_2015_day_21_input
new file mode 100644
index 0000000..db43742
--- /dev/null
+++ b/inputs/year_2015_day_21_input
@@ -0,0 +1,3 @@
+Hit Points: 100
+Damage: 8
+Armor: 2
diff --git a/src/year_2015.rs b/src/year_2015.rs
index 098f2c7..e9c78f9 100644
--- a/src/year_2015.rs
+++ b/src/year_2015.rs
@@ -53,6 +53,7 @@ pub mod day_17;
pub mod day_18;
pub mod day_19;
pub mod day_20;
+pub mod day_21;
/// Returns the solution for a specified exercise and input.
///
@@ -248,4 +249,11 @@ mod tests {
assert_eq!(day_20::day_20_v1(input), 831_600);
assert_eq!(day_20::day_20_v2(input), 884_520);
}
+
+ #[test]
+ fn day_21() {
+ let input = include_str!("../inputs/year_2015_day_21_input");
+ assert_eq!(day_21::day_21_v1(input), 91);
+ assert_eq!(day_21::day_21_v2(input), 158);
+ }
}
diff --git a/src/year_2015/day_21.rs b/src/year_2015/day_21.rs
new file mode 100644
index 0000000..e13bf78
--- /dev/null
+++ b/src/year_2015/day_21.rs
@@ -0,0 +1,233 @@
+//! Advent of Code 2015: Day 21: RPG Simulator 20XX
+
+struct Equipment {
+ cost: u8,
+ damage: u8,
+ armor: u8,
+}
+
+const WEAPONS: [Equipment; 5] = [
+ Equipment {
+ cost: 8,
+ damage: 4,
+ armor: 0,
+ }, // name: "Dagger"
+ Equipment {
+ cost: 10,
+ damage: 5,
+ armor: 0,
+ }, // name: "Shortsword"
+ Equipment {
+ cost: 25,
+ damage: 6,
+ armor: 0,
+ }, // name: "Warhammer"
+ Equipment {
+ cost: 40,
+ damage: 7,
+ armor: 0,
+ }, // name: "Longsword"
+ Equipment {
+ cost: 74,
+ damage: 8,
+ armor: 0,
+ }, // name: "Greataxe"
+];
+const ARMORS: [Equipment; 6] = [
+ Equipment {
+ cost: 0,
+ damage: 0,
+ armor: 0,
+ }, // name: "None"
+ Equipment {
+ cost: 13,
+ damage: 0,
+ armor: 1,
+ }, // name: "Leather"
+ Equipment {
+ cost: 31,
+ damage: 0,
+ armor: 2,
+ }, // name: "Chainmail"
+ Equipment {
+ cost: 53,
+ damage: 0,
+ armor: 3,
+ }, // name: "Splintmail"
+ Equipment {
+ cost: 75,
+ damage: 0,
+ armor: 4,
+ }, // name: "Bandedmail"
+ Equipment {
+ cost: 102,
+ damage: 0,
+ armor: 5,
+ }, // name: "Platemail"
+];
+const ACCESSORIES: [Equipment; 8] = [
+ Equipment {
+ cost: 0,
+ damage: 0,
+ armor: 0,
+ }, // name: "None"
+ Equipment {
+ cost: 0,
+ damage: 0,
+ armor: 0,
+ }, // name: "None"
+ Equipment {
+ cost: 25,
+ damage: 1,
+ armor: 0,
+ }, // name: "Damage +1"
+ Equipment {
+ cost: 50,
+ damage: 2,
+ armor: 0,
+ }, // name: "Damage +2"
+ Equipment {
+ cost: 100,
+ damage: 3,
+ armor: 0,
+ }, // name: "Damage +3"
+ Equipment {
+ cost: 20,
+ damage: 0,
+ armor: 1,
+ }, // name: "Defense +1"
+ Equipment {
+ cost: 40,
+ damage: 0,
+ armor: 2,
+ }, // name: "Defense +2"
+ Equipment {
+ cost: 80,
+ damage: 0,
+ armor: 3,
+ }, // name: "Defense +3"
+];
+
+const PLAYER_HP: u8 = 100;
+
+struct Character {
+ hit_points: u8,
+ damage: u8,
+ armor: u8,
+}
+
+impl Character {
+ fn from_equipment(equipment: [&Equipment; 4]) -> (usize, Self) {
+ let hit_points = PLAYER_HP;
+ let cost: usize = equipment.iter().map(|s| s.cost as usize).sum();
+ let damage: u8 = equipment.iter().map(|s| s.damage).sum();
+ let armor: u8 = equipment.iter().map(|s| s.armor).sum();
+
+ (
+ cost as usize,
+ Character {
+ hit_points,
+ damage,
+ armor,
+ },
+ )
+ }
+
+ fn from_string(boss_input: &str) -> Character {
+ let mut hit_points: u8 = 0;
+ let mut damage: u8 = 0;
+ let mut armor: u8 = 0;
+ for line in boss_input.lines() {
+ let parts: Vec<&str> = line.split(": ").collect();
+ let value = parts[1].parse::().unwrap();
+ match parts[0] {
+ "Hit Points" => hit_points = value,
+ "Damage" => damage = value,
+ "Armor" => armor = value,
+ _ => {
+ panic!("Invalid input: {}", line)
+ }
+ }
+ }
+
+ Character {
+ hit_points,
+ damage,
+ armor,
+ }
+ }
+}
+
+fn simulate_battle(player: &Character, boss: &Character) -> bool {
+ let player_damage: u8 = std::cmp::max(1, player.damage as i8 - boss.armor as i8) as u8;
+ let boss_damage: u8 = std::cmp::max(1, boss.damage as i8 - player.armor as i8) as u8;
+ let player_turns = (player.hit_points / boss_damage) + 1;
+ let boss_turns = (boss.hit_points / player_damage) + 1;
+
+ player_turns >= boss_turns
+}
+
+fn try_on_equipment(boss: &Character, initial_best: usize, price_updater: CMP) -> usize
+where
+ CMP: Fn(bool, usize, usize) -> usize,
+{
+ let mut best_price: usize = initial_best;
+ for weapon in WEAPONS.iter() {
+ for armor in ARMORS.iter() {
+ for acc_1 in ACCESSORIES[0..=6].iter() {
+ for acc_2 in ACCESSORIES[1..=7].iter() {
+ let (cost, player) = Character::from_equipment([
+ weapon,
+ armor,
+ acc_1,
+ acc_2,
+ ]);
+ let result = simulate_battle(&player, boss);
+ best_price = price_updater(result, best_price, cost);
+ }
+ }
+ }
+ }
+
+ best_price
+}
+
+pub fn day_21_v1<'a>(input: impl Into) -> usize {
+ let boss = Character::from_string(&input.into());
+ let price_updater = |result: bool, best_price, next_price| {
+ if result {
+ std::cmp::min(best_price, next_price)
+ } else {
+ best_price
+ }
+ };
+
+ try_on_equipment(&boss, 256, price_updater)
+}
+
+pub fn day_21_v2<'a>(input: impl Into) -> usize {
+ let boss = Character::from_string(&input.into());
+ let price_updater = |result: bool, best_price, next_price| {
+ if !result {
+ std::cmp::max(best_price, next_price)
+ } else {
+ best_price
+ }
+ };
+
+ try_on_equipment(&boss, 0, price_updater)
+}
+
+solvable!(day_21, day_21_v1, day_21_v2, usize);
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn wins_the_fight_with_samples_v1() {
+ let sample_boss = Character::from_string("Hit Points: 12\nDamage: 7\nArmor: 2");
+ let sample_player = Character::from_string("Hit Points: 8\nDamage: 5\nArmor: 5");
+ assert_eq!(simulate_battle(&sample_player, &sample_boss), true);
+ }
+}