rab_core/
build_search.rs

1use itertools::Itertools;
2use serde::{Deserialize, Serialize};
3use std::{
4    array,
5    cmp::{min, Ordering},
6    collections::HashMap,
7    iter,
8};
9
10use crate::armor_and_skills::{Armor, Gender, Skill};
11
12pub type Jewels = [Option<Skill>; 3];
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct Build {
16    pub helmet: Option<(Armor, Jewels)>,
17    pub chest: Option<(Armor, Jewels)>,
18    pub arm: Option<(Armor, Jewels)>,
19    pub waist: Option<(Armor, Jewels)>,
20    pub leg: Option<(Armor, Jewels)>,
21    pub talisman: Option<(Armor, Jewels)>,
22    pub weapon_jewels: Jewels,
23}
24
25impl Build {
26    pub fn get_all_skills_and_amounts(&self) -> HashMap<Skill, u8> {
27        let mut hm = HashMap::with_capacity(5);
28        for opt in array::IntoIter::new([
29            &self.helmet,
30            &self.chest,
31            &self.arm,
32            &self.waist,
33            &self.leg,
34            &self.talisman,
35        ])
36        .flatten()
37        {
38            let (armor, jewels) = opt;
39            for (skill, amount) in armor.skills.iter() {
40                if !hm.contains_key(skill) {
41                    hm.insert(*skill, 0);
42                }
43                *hm.get_mut(skill).unwrap() += amount
44            }
45            for jewel in jewels.iter().flatten() {
46                if !hm.contains_key(jewel) {
47                    hm.insert(*jewel, 0);
48                }
49                *hm.get_mut(jewel).unwrap() += 1
50            }
51        }
52        for jewel in self.weapon_jewels.iter().flatten() {
53            if !hm.contains_key(jewel) {
54                hm.insert(*jewel, 0);
55            }
56            *hm.get_mut(jewel).unwrap() += 1
57        }
58        hm
59    }
60}
61
62/// Returns a Vec containing every item of the provided slice but wrapped in an Option, then add None
63/// at the end of the Vec.
64/// # Why
65/// The main goal is to test every possible combination of armor pieces including the "empty" armor piece.
66/// The None represents this "empty" armor piece and if we need None, then every other pieces must be "optionified".
67fn optionify_slice_and_add_none<T>(slice: &[T]) -> Vec<Option<&T>> {
68    slice
69        .iter()
70        .map(Some)
71        .chain(iter::once(None::<&T>))
72        .collect()
73}
74
75/// Naive search of compatible builds. This is not recommended to call this function directly (see [`pre_selection_then_brute_force_search`]).
76/// It will try every possible build combination with the slices provided (thanks to a cartesian product).
77/// Then it will try to set jewels on the builds if the wishes are not yet fulfilled.
78/// # Detailed steps
79/// For each combination:
80/// 1. Remove from the wishes the combination's skills
81/// 2. Try to fulfill the remaining wishes with jewels without spoiling slots, if not possible the algorithm stops
82/// 3. Remove the repetitive found builds: If a build has the "same" armor pieces than another build
83/// but the first build has more empty armor pieces then we can ignore the second build. (Example in code)
84fn brute_force_search_builds(
85    wishes: &[(Skill, u8)],
86    all_armor_slices: AllArmorSlices,
87    weapon_slots: [u8; 3],
88) -> Vec<Build> {
89    let AllArmorSlices {
90        helmets,
91        chests,
92        arms,
93        waists,
94        legs,
95        talismans,
96    } = all_armor_slices;
97    let mut builds: Vec<Build> = Vec::with_capacity(500);
98
99    for v in array::IntoIter::new([helmets, chests, arms, waists, legs, talismans])
100        .map(optionify_slice_and_add_none)
101        .multi_cartesian_product()
102    {
103        let (helmet, chest, arm, waist, leg, talisman) = (v[0], v[1], v[2], v[3], v[4], v[5]);
104
105        // We remove from the wishes the skills that are already present
106        // in the armor. Then we know what are the jewels to set.
107        let mut delta_wishes: Vec<(Skill, u8)> = wishes.iter().copied().collect();
108        for &option in &[helmet, chest, arm, waist, leg, talisman] {
109            if let Some(armor) = option {
110                for &(skill, amount) in &armor.skills {
111                    for (w_skill, w_amount) in delta_wishes.iter_mut() {
112                        if skill == *w_skill {
113                            if amount > *w_amount {
114                                *w_amount = 0;
115                            } else {
116                                *w_amount -= amount;
117                            }
118                        }
119                    }
120                }
121            }
122        }
123        // reverse order sort
124        // We will place the bigger jewels first to be sure to not
125        // spoil big slots with little jewels
126        delta_wishes.sort_unstable_by(|(skill_a, _), (skill_b, _)| {
127            let jewel_size_a = skill_a.get_jewel_size();
128            let jewel_size_b = skill_b.get_jewel_size();
129            match (jewel_size_a, jewel_size_b) {
130                (None, None) => Ordering::Equal,
131                (None, Some(_)) => Ordering::Greater,
132                (Some(_), None) => Ordering::Less,
133                (Some(a), Some(b)) => b.cmp(&a),
134            }
135        });
136
137        const NB_PARTS: usize = 7;
138
139        let mut possible_jewels_for_each_part = [Jewels::default(); NB_PARTS];
140        let mut jewel_indices = [0; NB_PARTS];
141        let mut empty_armor_slots = [
142            extract_slots_copy(&helmet),
143            extract_slots_copy(&chest),
144            extract_slots_copy(&arm),
145            extract_slots_copy(&waist),
146            extract_slots_copy(&leg),
147            extract_slots_copy(&talisman),
148            weapon_slots,
149        ];
150
151        'wish_loop: for (skill, amount) in delta_wishes.iter_mut() {
152            for (jewel_slots, (jewels, index)) in empty_armor_slots.iter_mut().zip(
153                possible_jewels_for_each_part
154                    .iter_mut()
155                    .zip(jewel_indices.iter_mut()),
156            ) {
157                if *amount > 0 {
158                    for slot in jewel_slots.iter_mut() {
159                        if let Some(jewel_size) = skill.get_jewel_size() {
160                            if *slot >= jewel_size {
161                                *slot = 0;
162                                *amount -= 1;
163                                jewels[*index] = Some(*skill);
164                                *index += 1;
165                                if *amount == 0 {
166                                    // the wish is satisfied so we can
167                                    // skip to the next wish
168                                    continue 'wish_loop;
169                                }
170                            }
171                        } else {
172                            // We have a skill without a jewel (like Critical Boost)
173                            // rip in peace
174                            break 'wish_loop;
175                        }
176                    }
177                }
178            }
179        }
180
181        if delta_wishes.iter().all(|(_, amount)| *amount == 0) {
182            let build = Build {
183                helmet: helmet.map(|armor| (armor.clone(), possible_jewels_for_each_part[0])),
184                chest: chest.map(|armor| (armor.clone(), possible_jewels_for_each_part[1])),
185                arm: arm.map(|armor| (armor.clone(), possible_jewels_for_each_part[2])),
186                waist: waist.map(|armor| (armor.clone(), possible_jewels_for_each_part[3])),
187                leg: leg.map(|armor| (armor.clone(), possible_jewels_for_each_part[4])),
188                talisman: talisman.map(|armor| (armor.clone(), possible_jewels_for_each_part[5])),
189                weapon_jewels: possible_jewels_for_each_part[6],
190            };
191
192            // Avoid having redondant builds like:
193            // A B C D E
194            // and
195            // A B None D E
196            // If the build with the None works, then it's useless to keep the first build
197            // Naive algorithm ahead
198
199            let mut push_it = true;
200            let mut replacement: Option<&mut Build> = None;
201            // maybe less than needed, maybe overkill. I don't really know
202            let mut to_remove = Vec::with_capacity(20);
203
204            for (key, old_build) in builds.iter_mut().enumerate() {
205                let mut old_has_better_none = false;
206                let mut new_has_better_none = false;
207                // don't want to use .iter() because it will give &&Option<>
208                // and don't want to use [build.helmet, build.chest, ...].iter() because it will copy
209                // the elements (they don't even implement the Copy trait)
210                for couple in array::IntoIter::new([
211                    &build.helmet,
212                    &build.chest,
213                    &build.arm,
214                    &build.waist,
215                    &build.leg,
216                    &build.talisman,
217                ])
218                .zip([
219                    &old_build.helmet,
220                    &old_build.chest,
221                    &old_build.arm,
222                    &old_build.waist,
223                    &old_build.leg,
224                    &old_build.talisman,
225                ]) {
226                    match couple {
227                        (None, Some(_)) => {
228                            new_has_better_none = true;
229                            if old_has_better_none {
230                                // if they have None at different places
231                                break;
232                            }
233                        }
234                        (Some(_), None) => {
235                            old_has_better_none = true;
236                            if new_has_better_none {
237                                break;
238                            }
239                        }
240                        (Some(part), Some(old_part)) if part.0 != old_part.0 => {
241                            // they are not comparable
242                            old_has_better_none = new_has_better_none;
243                            break;
244                        }
245                        _ => {}
246                    };
247                }
248
249                if old_has_better_none != new_has_better_none {
250                    push_it = false;
251                    if old_has_better_none {
252                        break;
253                    }
254                    // have to move this reference
255                    // out of the loop to please the
256                    // compiler
257                    if replacement.is_none() {
258                        replacement = Some(old_build);
259                    } else {
260                        // We continue to search builds worse
261                        // than the new part (yes this is possible)
262                        to_remove.push(key);
263                    }
264                }
265            }
266            if let Some(place) = replacement {
267                *place = build;
268            } else if push_it {
269                builds.push(build);
270            }
271            to_remove.sort_unstable();
272
273            // thank you https://stackoverflow.com/a/57948703
274            for index in to_remove.drain(..).rev() {
275                builds.swap_remove(index);
276            }
277        }
278    }
279
280    builds
281}
282
283pub struct AllArmorSlices<'a> {
284    pub helmets: &'a [Armor],
285    pub chests: &'a [Armor],
286    pub arms: &'a [Armor],
287    pub waists: &'a [Armor],
288    pub legs: &'a [Armor],
289    pub talismans: &'a [Armor],
290}
291
292/// Makes a copy of the armor slots. Useful when we want to write in the array.
293fn extract_slots_copy(helmet: &Option<&Armor>) -> [u8; 3] {
294    match helmet {
295        Some(armor) => {
296            let mut slots = [0; 3];
297            for (key, &slot) in armor.slots.iter().enumerate() {
298                slots[key] = slot;
299            }
300            slots
301        }
302        None => [0; 3],
303    }
304}
305
306/// Calls [`search_best_candidates`] on each slice before calling [`brute_force_search_builds`]. Returns
307/// the results of the brute force search. This is the recommended function to call when we want to search builds.
308pub fn pre_selection_then_brute_force_search(
309    wishes: &[(Skill, u8)],
310    all_armor_slices: AllArmorSlices,
311    gender: Gender,
312    weapon_slots: [u8; 3],
313) -> Vec<Build> {
314    let AllArmorSlices {
315        helmets,
316        chests,
317        arms,
318        waists,
319        legs,
320        talismans,
321    } = all_armor_slices;
322    brute_force_search_builds(
323        wishes,
324        AllArmorSlices {
325            helmets: &search_best_candidates(wishes, helmets, gender),
326            chests: &search_best_candidates(wishes, chests, gender),
327            arms: &search_best_candidates(wishes, arms, gender),
328            waists: &search_best_candidates(wishes, waists, gender),
329            legs: &search_best_candidates(wishes, legs, gender),
330            talismans: &search_best_candidates(wishes, talismans, gender),
331        },
332        weapon_slots,
333    )
334}
335
336/// When we are comparing armor pieces we can't really say that an armor is "equal" to another.
337/// Like a piece with two 1-sized slots and another with one 3-sized slot. We can't really say
338/// that the first piece is better than the other and vice-versa and we can't say that they are equal either.
339///
340/// As an example, let's say that we are comparing three pieces:
341/// - piece A with two 1-sized slots
342/// - piece B with one 3-sized slots
343/// - piece C with one 1-sized slot and one 2-sized slot
344///
345/// Let's say that if we can't say which piece is the best between two pieces then the two pieces are equal.
346///
347/// Then A == B and B == C. But if this is true, is A == C? No, because C is clearly better than A. This is why
348/// this is "strange" to use directly [`Ordering`] with [`Ordering::Equal`].
349#[derive(Eq, PartialEq, Debug)]
350enum OddComparison {
351    Better,
352    Worse,
353    Undefined,
354}
355
356/// The most difficult function in my opinion (ityt). Compares two armor pieces and returns an [`OddComparison`].
357/// # Detailed steps
358/// 1. Generate a "delta" skill for each piece. See [`generate_deltas_skills`].
359/// 2. Generate virtual slots for each piece. See [`generate_virtual_slots`].
360/// 3. We compare the pieces. Some details in code.
361///
362/// Special comparison: If A can recreate B's skills with jewels and A has still equal or better
363/// free slots than B's then A is better. This can change because jewels are expensive and sometimes
364/// we just want the piece that have already the skills.
365fn compare_armors(wishes: &[(Skill, u8)], a: &Armor, b: &Armor) -> OddComparison {
366    // add virtual slot depending on the wished skills,
367    // if the skill has no jewel (like Critical Boost), the armor has top priority
368    let (delta_skills_a, delta_skills_b) = generate_deltas_skills(&a.skills, &b.skills);
369    let (priority_a, virtual_slots_a) = generate_virtual_slots(wishes, &delta_skills_a);
370    let (priority_b, virtual_slots_b) = generate_virtual_slots(wishes, &delta_skills_b);
371
372    let mut slots_a_copy = Vec::with_capacity(3);
373    let mut slots_b_copy = Vec::with_capacity(3);
374
375    if a.slots.len() + virtual_slots_a.len() <= b.slots.len() {
376        for &s in &[a.slots.as_slice(), virtual_slots_a.as_slice()].concat() {
377            slots_a_copy.push(s);
378        }
379        for &s in &b.slots {
380            slots_b_copy.push(s);
381        }
382        slots_a_copy.sort_unstable();
383        slots_b_copy.sort_unstable();
384        // if this is the case it means that b has the same slots as
385        // a's virtual slots (or better), so b is more flexible than a because
386        // we can recreate a's skills by giving the good jewel to b except if a has top priority
387        // the (slots_a_copy == slots_b_copy && a.slots.len() < b.slots.len()) means:
388        // if real&virtual slots from a are the same as real slots from b
389        // and a has less real slots than b, then a is worse than b
390        // we can't use the OddComparison::Undefined in this case
391        if !priority_a
392            && ((slots_a_copy == slots_b_copy && a.slots.len() < b.slots.len())
393                || compare_slots(&slots_a_copy, &slots_b_copy) == OddComparison::Worse)
394        {
395            return OddComparison::Worse;
396        }
397    } else if b.slots.len() + virtual_slots_b.len() <= a.slots.len() {
398        for &s in &a.slots {
399            slots_a_copy.push(s);
400        }
401        for &s in &[b.slots.as_slice(), virtual_slots_b.as_slice()].concat() {
402            slots_b_copy.push(s);
403        }
404        slots_a_copy.sort_unstable();
405        slots_b_copy.sort_unstable();
406        if !priority_b
407            && ((slots_a_copy == slots_b_copy && b.slots.len() < a.slots.len())
408                || compare_slots(&slots_a_copy, &slots_b_copy) == OddComparison::Better)
409        {
410            return OddComparison::Better;
411        }
412    }
413
414    OddComparison::Undefined
415}
416
417/// Finds the best pieces to fulfill the wishes.
418/// # Detailed steps
419/// 1. Remove the pieces that are not compatible with the hunter's gender.
420/// 2. Remove the pieces that have not the wished skills and can't accept a jewel for these skills.
421/// 3. Compare the selected pieces with [`compare_armors`] and remove the pieces that are worse than another piece.
422fn search_best_candidates(wishes: &[(Skill, u8)], armors: &[Armor], gender: Gender) -> Vec<Armor> {
423    // trivial sort
424    let armors: Vec<&Armor> = armors
425        .iter()
426        .filter(|armor| {
427            if let Some(armor_gender) = armor.gender {
428                gender == armor_gender
429            } else {
430                true
431            }
432        })
433        .filter(|armor| {
434            for (skill, _) in wishes {
435                // check if the armor can accept a jewel for one of the wanted skills
436                for &slot in &armor.slots {
437                    if let Some(size) = skill.get_jewel_size() {
438                        if slot >= size {
439                            return true;
440                        }
441                    }
442                }
443                // check if the armor has one of the wanted skills
444                for (armor_skill, _) in &armor.skills {
445                    if armor_skill == skill {
446                        return true;
447                    }
448                }
449            }
450            false
451        })
452        .collect();
453
454    let mut armors_copy = Vec::with_capacity(armors.len());
455    for &w in &armors {
456        armors_copy.push(w.clone());
457    }
458    // non trivial sort
459    armors_copy.retain(|a| {
460        for &b in &armors {
461            if compare_armors(wishes, a, b) == OddComparison::Worse {
462                return false;
463            }
464        }
465        true
466    });
467
468    armors_copy
469}
470
471/// Creates dummy slots from skills' jewel size. Returns (true, _) if one of the skill doesn't
472/// have a jewel.
473/// # Why
474/// This is useful when we want to know if a piece with only its slots and jewels can be better or equal than
475/// another with its true skills and slots.
476fn generate_virtual_slots(wishes: &[(Skill, u8)], skills: &[(Skill, u8)]) -> (bool, Vec<u8>) {
477    let mut priority = false;
478    let mut virtual_slots = Vec::with_capacity(5);
479    for (wished_skill, _) in wishes {
480        for (skill, amount) in skills {
481            if skill == wished_skill {
482                if let Some(size) = skill.get_jewel_size() {
483                    for _ in 0..*amount {
484                        virtual_slots.push(size);
485                    }
486                } else if *amount > 0 {
487                    // we check the amount because in a "delta skill" an amount may equal 0
488                    priority = true;
489                }
490            }
491        }
492    }
493    (priority, virtual_slots)
494}
495
496/// Compares two pieces' slot and returns an [`OddComparison`].
497/// See [`OddComparison`] for details.
498///
499/// # Warning
500/// This functions works only if the provided slices are sorted.
501fn compare_slots(slots0: &[u8], slots1: &[u8]) -> OddComparison {
502    if slots0.is_empty() {
503        if slots1.is_empty() {
504            return OddComparison::Undefined;
505        }
506        return OddComparison::Worse;
507    }
508    if slots1.is_empty() {
509        return OddComparison::Better;
510    }
511    let mut slot_cmp: Vec<i8> = vec![0; 3];
512    for (key, &value) in slots0.iter().enumerate() {
513        // +3 because we shift the stuff if there is not enough slots to
514        // make the comparison easier
515        slot_cmp[key + 3 - slots0.len()] = value as i8;
516    }
517    for (key, &value) in slots1.iter().enumerate() {
518        slot_cmp[key + 3 - slots1.len()] -= value as i8;
519    }
520    let mut sign = 0;
521    for i in slot_cmp {
522        if i != 0 {
523            if sign == 0 {
524                sign = i;
525            } else if sign.signum() != i.signum() {
526                // if the signs are different it means that
527                // the two armors has not very comparable slots
528                // like 1,1,1 and 0,0,3
529                // => 1-0, 1-0, 1- 3 = different signs
530                // or 2,2,2 and 1,3,3
531                // having 3 2-sized slots can have a different use
532                // than having 2 3-sized slots and 1 1-sized slot
533                return OddComparison::Undefined;
534            }
535        }
536    }
537    if sign == 0 {
538        OddComparison::Undefined
539    } else if sign.signum() == -1 {
540        OddComparison::Worse
541    } else {
542        OddComparison::Better
543    }
544}
545
546/// Compares skills of each piece and removes identical skills from them.
547/// # Why
548/// When we are comparing two armor pieces, it becomes easier when we remove
549/// the same skills to only compare the true differences.
550///
551/// As an example: let's say with have A with the skills [(Botanist,2),(CriticalBoost,1)]
552/// and B with [(Botanist,1),(CriticalBoost,1),(AttackBoost,1)]
553///
554/// the delta skills will be:
555/// - for A: [(Botanist,1),(CriticalBoost,0)]
556/// - for B: [(Botanist,0),(CriticalBoost,0),(AttackBoost,1)]
557fn generate_deltas_skills(skills0: &[(Skill, u8)], skills1: &[(Skill, u8)]) -> DeltasSkills {
558    let mut delta0 = Vec::with_capacity(skills0.len());
559    let mut delta1 = Vec::with_capacity(skills1.len());
560
561    for &s in skills0 {
562        delta0.push(s);
563    }
564    for &s in skills1 {
565        delta1.push(s);
566    }
567
568    let mut to_do0 = Vec::with_capacity(3);
569    let mut to_do1 = Vec::with_capacity(3);
570
571    for (key0, &(skill0, amount0)) in delta0.iter().enumerate() {
572        for (key1, &(skill1, amount1)) in delta1.iter().enumerate() {
573            if skill0 == skill1 {
574                let delta = min(amount0, amount1);
575                //to please the borrow checker
576                to_do0.push((key0, (skill0, amount0 - delta)));
577                to_do1.push((key1, (skill0, amount1 - delta)));
578            }
579        }
580    }
581
582    for &(key, couple) in &to_do0 {
583        delta0[key] = couple;
584    }
585    for &(key, couple) in &to_do1 {
586        delta1[key] = couple;
587    }
588
589    (delta0, delta1)
590}
591
592type DeltasSkills = (Vec<(Skill, u8)>, Vec<(Skill, u8)>);