1use crate::util::parse::*;
45use std::ops::{Add, Sub};
46
47const ZERO: Mineral = Mineral::from(0, 0, 0, 0);
49const ORE_BOT: Mineral = Mineral::from(1, 0, 0, 0);
50const CLAY_BOT: Mineral = Mineral::from(0, 1, 0, 0);
51const OBSIDIAN_BOT: Mineral = Mineral::from(0, 0, 1, 0);
52const GEODE_BOT: Mineral = Mineral::from(0, 0, 0, 1);
53
54#[derive(Clone, Copy)]
55struct Mineral {
56    ore: u32,
57    clay: u32,
58    obsidian: u32,
59    geode: u32,
60}
61
62impl Mineral {
63    const fn from(ore: u32, clay: u32, obsidian: u32, geode: u32) -> Self {
64        Mineral { ore, clay, obsidian, geode }
65    }
66
67    fn less_than_equal(self, rhs: Self) -> bool {
69        self.ore <= rhs.ore && self.clay <= rhs.clay && self.obsidian <= rhs.obsidian
70    }
71}
72
73impl Add for Mineral {
75    type Output = Self;
76
77    fn add(self, rhs: Self) -> Self {
78        Mineral {
79            ore: self.ore + rhs.ore,
80            clay: self.clay + rhs.clay,
81            obsidian: self.obsidian + rhs.obsidian,
82            geode: self.geode + rhs.geode,
83        }
84    }
85}
86
87impl Sub for Mineral {
88    type Output = Self;
89
90    fn sub(self, rhs: Self) -> Self {
91        Mineral {
92            ore: self.ore - rhs.ore,
93            clay: self.clay - rhs.clay,
94            obsidian: self.obsidian - rhs.obsidian,
95            geode: self.geode - rhs.geode,
96        }
97    }
98}
99
100pub struct Blueprint {
101    id: u32,
102    max_ore: u32,
103    max_clay: u32,
104    max_obsidian: u32,
105    ore_cost: Mineral,
106    clay_cost: Mineral,
107    obsidian_cost: Mineral,
108    geode_cost: Mineral,
109}
110
111impl Blueprint {
112    fn from(chunk: [u32; 7]) -> Self {
113        let [id, ore1, ore2, ore3, clay, ore4, obsidian] = chunk;
114        Blueprint {
115            id,
116            max_ore: ore1.max(ore2).max(ore3).max(ore4),
117            max_clay: clay,
118            max_obsidian: obsidian,
119            ore_cost: Mineral::from(ore1, 0, 0, 0),
120            clay_cost: Mineral::from(ore2, 0, 0, 0),
121            obsidian_cost: Mineral::from(ore3, clay, 0, 0),
122            geode_cost: Mineral::from(ore4, 0, obsidian, 0),
123        }
124    }
125}
126
127pub fn parse(input: &str) -> Vec<Blueprint> {
128    input
129        .iter_unsigned()
130        .collect::<Vec<_>>()
131        .chunks_exact(7)
132        .map(|slice| Blueprint::from(slice.try_into().unwrap()))
133        .collect()
134}
135
136pub fn part1(input: &[Blueprint]) -> u32 {
137    input.iter().map(|blueprint| blueprint.id * maximize(blueprint, 24)).sum()
138}
139
140pub fn part2(input: &[Blueprint]) -> u32 {
141    input.iter().take(3).map(|blueprint| maximize(blueprint, 32)).product()
142}
143
144fn maximize(blueprint: &Blueprint, time: u32) -> u32 {
145    let mut result = 0;
146    dfs(blueprint, &mut result, time, ORE_BOT, ZERO);
147    result
148}
149
150fn dfs(blueprint: &Blueprint, result: &mut u32, time: u32, bots: Mineral, resources: Mineral) {
152    *result = (*result).max(resources.geode + bots.geode * time);
154
155    if heuristic(blueprint, *result, time, bots, resources) {
157        if bots.obsidian > 0 && time > 1 {
158            next(blueprint, result, time, bots, resources, GEODE_BOT, blueprint.geode_cost);
159        }
160        if bots.obsidian < blueprint.max_obsidian && bots.clay > 0 && time > 3 {
161            next(blueprint, result, time, bots, resources, OBSIDIAN_BOT, blueprint.obsidian_cost);
162        }
163        if bots.ore < blueprint.max_ore && time > 3 {
164            next(blueprint, result, time, bots, resources, ORE_BOT, blueprint.ore_cost);
165        }
166        if bots.clay < blueprint.max_clay && time > 5 {
167            next(blueprint, result, time, bots, resources, CLAY_BOT, blueprint.clay_cost);
168        }
169    }
170}
171
172#[inline]
179fn heuristic(
180    blueprint: &Blueprint,
181    result: u32,
182    time: u32,
183    mut bots: Mineral,
184    mut resources: Mineral,
185) -> bool {
186    for _ in 0..time {
187        resources.ore = blueprint.max_ore;
189
190        if blueprint.geode_cost.less_than_equal(resources) {
192            resources = resources + bots - blueprint.geode_cost;
193            bots = bots + GEODE_BOT;
194        } else if blueprint.obsidian_cost.less_than_equal(resources) {
195            resources = resources + bots - blueprint.obsidian_cost;
196            bots = bots + OBSIDIAN_BOT;
197        } else {
198            resources = resources + bots;
199        }
200
201        bots = bots + CLAY_BOT;
203    }
204
205    resources.geode > result
206}
207
208#[inline]
211fn next(
212    blueprint: &Blueprint,
213    result: &mut u32,
214    time: u32,
215    bots: Mineral,
216    mut resources: Mineral,
217    new_bot: Mineral,
218    cost: Mineral,
219) {
220    for jump in 1..time {
221        if cost.less_than_equal(resources) {
222            dfs(blueprint, result, time - jump, bots + new_bot, resources + bots - cost);
223            break;
224        }
225        resources = resources + bots;
226    }
227}