158 lines
3.8 KiB
Rust
158 lines
3.8 KiB
Rust
//! Day 7: Camel Cards
|
|
|
|
use std::{
|
|
cmp::Ordering,
|
|
collections::{BTreeSet, HashMap},
|
|
};
|
|
|
|
use itertools::Itertools;
|
|
|
|
pub const DAY: u8 = 7;
|
|
pub const TITLE: &str = "Camel Cards";
|
|
|
|
#[derive(Debug, PartialEq, Eq)]
|
|
struct Hand(String);
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
|
enum HandType {
|
|
HighCard,
|
|
OnePair,
|
|
TwoPair,
|
|
ThreeOfAKind,
|
|
FullHouse,
|
|
FourOfAKind,
|
|
FiveOfAKind,
|
|
}
|
|
|
|
fn card_value(c: char, v2: bool) -> u8 {
|
|
if let Some(d) = c.to_digit(10) {
|
|
d as u8
|
|
} else {
|
|
match c {
|
|
'T' => 10,
|
|
'J' => {
|
|
if v2 {
|
|
1
|
|
} else {
|
|
11
|
|
}
|
|
}
|
|
'Q' => 12,
|
|
'K' => 13,
|
|
'A' => 14,
|
|
_ => panic!("invalid card `{c}`"),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Hand {
|
|
fn hand_type(&self, v2: bool) -> HandType {
|
|
let mut card_counts = self.0.chars().counts();
|
|
let mut joker_count = if v2 {
|
|
card_counts.remove(&'J').unwrap_or_default()
|
|
} else {
|
|
0
|
|
};
|
|
let card_counts_sorted = card_counts.values().cloned().sorted().collect_vec();
|
|
|
|
let mut cc_highest = card_counts_sorted.last().cloned().unwrap_or_default();
|
|
let jokers_used = joker_count.min(5 - cc_highest);
|
|
cc_highest += jokers_used;
|
|
joker_count -= jokers_used;
|
|
|
|
let cc_second_highest = card_counts_sorted
|
|
.len()
|
|
.checked_sub(2)
|
|
.and_then(|i| card_counts_sorted.get(i))
|
|
.cloned()
|
|
.unwrap_or_default()
|
|
+ joker_count;
|
|
|
|
match cc_highest {
|
|
5 => HandType::FiveOfAKind,
|
|
4 => HandType::FourOfAKind,
|
|
3 => match cc_second_highest {
|
|
2 => HandType::FullHouse,
|
|
_ => HandType::ThreeOfAKind,
|
|
},
|
|
2 => match cc_second_highest {
|
|
2 => HandType::TwoPair,
|
|
_ => HandType::OnePair,
|
|
},
|
|
1 => HandType::HighCard,
|
|
_ => panic!("invalid hand `{}`", self.0),
|
|
}
|
|
}
|
|
|
|
fn compare1(&self, other: &Self, v2: bool) -> Ordering {
|
|
self.hand_type(v2).cmp(&other.hand_type(v2)).then_with(|| {
|
|
self.0
|
|
.chars()
|
|
.zip(other.0.chars())
|
|
.find_map(|(a, b)| {
|
|
let r = card_value(a, v2).cmp(&card_value(b, v2));
|
|
if r.is_ne() {
|
|
Some(r)
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.unwrap_or(Ordering::Equal)
|
|
})
|
|
}
|
|
}
|
|
|
|
fn parse(input: Vec<String>) -> Vec<(Hand, u32)> {
|
|
input
|
|
.into_iter()
|
|
.map(|l| {
|
|
let (a, b) = l.split_once(' ').unwrap();
|
|
(Hand(a.to_owned()), b.parse().unwrap())
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
fn solution(input: Vec<String>, v2: bool) -> String {
|
|
let parsed = parse(input);
|
|
|
|
parsed
|
|
.iter()
|
|
.sorted_by(|(h1, _), (h2, _)| h1.compare1(h2, v2))
|
|
.enumerate()
|
|
.map(|(rank, (_, bid))| (rank as u32 + 1) * bid)
|
|
.sum::<u32>()
|
|
.to_string()
|
|
}
|
|
|
|
pub fn part1(input: Vec<String>) -> String {
|
|
solution(input, false)
|
|
}
|
|
|
|
pub fn part2(input: Vec<String>) -> String {
|
|
solution(input, true)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn t_example1() {
|
|
assert_eq!(part1(crate::read_example(DAY, 1)), "6440");
|
|
}
|
|
|
|
#[test]
|
|
fn t_part1() {
|
|
assert_eq!(part1(crate::read_input(DAY)), "253933213");
|
|
}
|
|
|
|
#[test]
|
|
fn t_example2() {
|
|
assert_eq!(part2(crate::read_example(DAY, 2)), "5905");
|
|
}
|
|
|
|
#[test]
|
|
fn t_part2() {
|
|
assert_eq!(part2(crate::read_input(DAY)), "253473930");
|
|
}
|
|
}
|